rustywallet_mnemonic/wordlist/
mod.rs

1//! BIP39 wordlist implementations.
2//!
3//! This module provides wordlists for multiple languages as defined in BIP39.
4//! Each wordlist contains exactly 2048 words.
5
6mod chinese_simplified;
7mod english;
8mod japanese;
9mod korean;
10mod spanish;
11
12pub use chinese_simplified::CHINESE_SIMPLIFIED_WORDLIST;
13pub use english::ENGLISH_WORDLIST;
14pub use japanese::JAPANESE_WORDLIST;
15pub use korean::KOREAN_WORDLIST;
16pub use spanish::SPANISH_WORDLIST;
17
18/// Supported languages for mnemonic generation.
19///
20/// BIP39 defines wordlists for multiple languages. Each wordlist contains
21/// exactly 2048 words that are used to encode entropy into a mnemonic phrase.
22///
23/// # Example
24///
25/// ```
26/// use rustywallet_mnemonic::Language;
27///
28/// let lang = Language::English;
29/// assert!(lang.contains("abandon"));
30///
31/// let lang = Language::Japanese;
32/// assert!(lang.contains("あいこくしん"));
33/// ```
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
35pub enum Language {
36    /// English wordlist (BIP39 standard)
37    #[default]
38    English,
39    /// Japanese wordlist (BIP39)
40    Japanese,
41    /// Spanish wordlist (BIP39)
42    Spanish,
43    /// Chinese Simplified wordlist (BIP39)
44    ChineseSimplified,
45    /// Korean wordlist (BIP39)
46    Korean,
47}
48
49impl Language {
50    /// Get all supported languages.
51    pub fn all() -> &'static [Language] {
52        &[
53            Language::English,
54            Language::Japanese,
55            Language::Spanish,
56            Language::ChineseSimplified,
57            Language::Korean,
58        ]
59    }
60
61    /// Get the wordlist for this language.
62    pub fn wordlist(&self) -> &'static [&'static str; 2048] {
63        match self {
64            Language::English => &ENGLISH_WORDLIST,
65            Language::Japanese => &JAPANESE_WORDLIST,
66            Language::Spanish => &SPANISH_WORDLIST,
67            Language::ChineseSimplified => &CHINESE_SIMPLIFIED_WORDLIST,
68            Language::Korean => &KOREAN_WORDLIST,
69        }
70    }
71
72    /// Get word at index (0-2047).
73    pub fn get_word(&self, index: usize) -> Option<&'static str> {
74        if index < 2048 {
75            Some(self.wordlist()[index])
76        } else {
77            None
78        }
79    }
80
81    /// Get index of word in wordlist.
82    pub fn get_index(&self, word: &str) -> Option<usize> {
83        let word_lower = word.to_lowercase();
84        self.wordlist().iter().position(|&w| w == word_lower)
85    }
86
87    /// Check if word exists in wordlist.
88    pub fn contains(&self, word: &str) -> bool {
89        self.get_index(word).is_some()
90    }
91
92    /// Get the language name as a string.
93    pub fn name(&self) -> &'static str {
94        match self {
95            Language::English => "English",
96            Language::Japanese => "Japanese",
97            Language::Spanish => "Spanish",
98            Language::ChineseSimplified => "Chinese (Simplified)",
99            Language::Korean => "Korean",
100        }
101    }
102
103    /// Detect language from a single word.
104    ///
105    /// Returns the first language that contains the word.
106    /// Note: Some words may exist in multiple wordlists.
107    pub fn detect_from_word(word: &str) -> Option<Language> {
108        for lang in Self::all() {
109            if lang.contains(word) {
110                return Some(*lang);
111            }
112        }
113        None
114    }
115
116    /// Detect language from a phrase by checking all words.
117    ///
118    /// Returns the language where all words are found in the wordlist.
119    /// Returns None if no single language contains all words.
120    pub fn detect_from_phrase(phrase: &str) -> Option<Language> {
121        let words: Vec<&str> = phrase.split_whitespace().collect();
122        if words.is_empty() {
123            return None;
124        }
125
126        for lang in Self::all() {
127            let all_match = words.iter().all(|word| lang.contains(word));
128            if all_match {
129                return Some(*lang);
130            }
131        }
132        None
133    }
134}
135
136impl std::fmt::Display for Language {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        write!(f, "{}", self.name())
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_english_wordlist() {
148        let lang = Language::English;
149        assert_eq!(lang.wordlist().len(), 2048);
150        assert_eq!(lang.get_word(0), Some("abandon"));
151        assert_eq!(lang.get_word(2047), Some("zoo"));
152        assert!(lang.contains("abandon"));
153        assert!(!lang.contains("notaword"));
154    }
155
156    #[test]
157    fn test_japanese_wordlist() {
158        let lang = Language::Japanese;
159        assert_eq!(lang.wordlist().len(), 2048);
160        assert_eq!(lang.get_word(0), Some("あいこくしん"));
161        assert!(lang.contains("あいこくしん"));
162    }
163
164    #[test]
165    fn test_spanish_wordlist() {
166        let lang = Language::Spanish;
167        assert_eq!(lang.wordlist().len(), 2048);
168        assert_eq!(lang.get_word(0), Some("ábaco"));
169        assert!(lang.contains("ábaco"));
170    }
171
172    #[test]
173    fn test_chinese_simplified_wordlist() {
174        let lang = Language::ChineseSimplified;
175        assert_eq!(lang.wordlist().len(), 2048);
176        assert_eq!(lang.get_word(0), Some("的"));
177        assert!(lang.contains("的"));
178    }
179
180    #[test]
181    fn test_korean_wordlist() {
182        let lang = Language::Korean;
183        assert_eq!(lang.wordlist().len(), 2048);
184        assert_eq!(lang.get_word(0), Some("가격"));
185        assert!(lang.contains("가격"));
186    }
187
188    #[test]
189    fn test_detect_from_word() {
190        assert_eq!(Language::detect_from_word("abandon"), Some(Language::English));
191        assert_eq!(Language::detect_from_word("あいこくしん"), Some(Language::Japanese));
192        assert_eq!(Language::detect_from_word("ábaco"), Some(Language::Spanish));
193        assert_eq!(Language::detect_from_word("的"), Some(Language::ChineseSimplified));
194        assert_eq!(Language::detect_from_word("가격"), Some(Language::Korean));
195        assert_eq!(Language::detect_from_word("notaword"), None);
196    }
197
198    #[test]
199    fn test_detect_from_phrase() {
200        let english_phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
201        assert_eq!(Language::detect_from_phrase(english_phrase), Some(Language::English));
202    }
203
204    #[test]
205    fn test_all_languages() {
206        let all = Language::all();
207        assert_eq!(all.len(), 5);
208        assert!(all.contains(&Language::English));
209        assert!(all.contains(&Language::Japanese));
210        assert!(all.contains(&Language::Spanish));
211        assert!(all.contains(&Language::ChineseSimplified));
212        assert!(all.contains(&Language::Korean));
213    }
214}