Skip to main content

iced_code_editor/
i18n.rs

1//! Internationalization support for the code editor.
2//!
3//! This module provides translation support for UI text in the search dialog.
4//!
5//! # Using rust-i18n
6//!
7//! The translations are available in YAML files in the `locales` directory.
8//! The `rust-i18n` crate is integrated and can be used directly via the `t!` macro:
9//!
10//! ```ignore
11//! use iced_code_editor::t;
12//!
13//! // Use translations directly
14//! let text = t!("search.placeholder");
15//! ```
16
17/// Supported languages for the code editor UI.
18///
19/// # Examples
20///
21/// ```
22/// use iced_code_editor::Language;
23///
24/// let lang = Language::English;
25/// assert_eq!(lang, Language::default());
26/// ```
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum Language {
29    /// English language
30    #[default]
31    English,
32    /// French language
33    French,
34    /// Spanish language
35    Spanish,
36    /// German language
37    German,
38    /// Italian language
39    Italian,
40    /// Portuguese (Brazilian) language
41    PortugueseBR,
42    /// Portuguese (European) language
43    PortuguesePT,
44    /// Simplified Chinese language
45    ChineseSimplified,
46}
47
48impl Language {
49    /// Returns the locale code for this language.
50    ///
51    /// # Examples
52    ///
53    /// ```
54    /// use iced_code_editor::Language;
55    ///
56    /// assert_eq!(Language::English.to_locale(), "en");
57    /// assert_eq!(Language::French.to_locale(), "fr");
58    /// assert_eq!(Language::Spanish.to_locale(), "es");
59    /// assert_eq!(Language::German.to_locale(), "de");
60    /// assert_eq!(Language::Italian.to_locale(), "it");
61    /// assert_eq!(Language::PortugueseBR.to_locale(), "pt-BR");
62    /// assert_eq!(Language::PortuguesePT.to_locale(), "pt-PT");
63    /// ```
64    #[must_use]
65    pub const fn to_locale(self) -> &'static str {
66        match self {
67            Self::English => "en",
68            Self::French => "fr",
69            Self::Spanish => "es",
70            Self::German => "de",
71            Self::Italian => "it",
72            Self::PortugueseBR => "pt-BR",
73            Self::PortuguesePT => "pt-PT",
74            Self::ChineseSimplified => "zh-CN",
75        }
76    }
77}
78
79/// Provides translated text strings for UI elements.
80///
81/// This struct contains all UI text translations used in the search dialog,
82/// including placeholders, tooltips, and labels.
83///
84/// # Examples
85///
86/// ```
87/// use iced_code_editor::{Language, Translations};
88///
89/// let translations = Translations::new(Language::French);
90/// assert_eq!(translations.search_placeholder(), "Rechercher...");
91/// ```
92#[derive(Debug, Clone, Copy, Default)]
93pub struct Translations {
94    language: Language,
95}
96
97impl Translations {
98    /// Creates a new `Translations` instance with the specified language.
99    ///
100    /// This sets the global rust-i18n locale to the specified language.
101    ///
102    /// # Examples
103    ///
104    /// ```
105    /// use iced_code_editor::{Language, Translations};
106    ///
107    /// let translations = Translations::new(Language::Spanish);
108    /// assert_eq!(translations.language(), Language::Spanish);
109    /// ```
110    #[must_use]
111    pub fn new(language: Language) -> Self {
112        rust_i18n::set_locale(language.to_locale());
113        Self { language }
114    }
115
116    /// Returns the current language.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// use iced_code_editor::{Language, Translations};
122    ///
123    /// let translations = Translations::new(Language::French);
124    /// assert_eq!(translations.language(), Language::French);
125    /// ```
126    #[must_use]
127    pub const fn language(&self) -> Language {
128        self.language
129    }
130
131    /// Sets the language for translations.
132    ///
133    /// This updates the global rust-i18n locale.
134    ///
135    /// # Examples
136    ///
137    /// ```
138    /// use iced_code_editor::{Language, Translations};
139    ///
140    /// let mut translations = Translations::new(Language::English);
141    /// translations.set_language(Language::Spanish);
142    /// assert_eq!(translations.language(), Language::Spanish);
143    /// ```
144    pub fn set_language(&mut self, language: Language) {
145        self.language = language;
146        rust_i18n::set_locale(language.to_locale());
147    }
148
149    /// Returns the placeholder text for the search input field.
150    ///
151    /// # Examples
152    ///
153    /// ```
154    /// use iced_code_editor::{Language, Translations};
155    ///
156    /// let en = Translations::new(Language::English);
157    /// assert_eq!(en.search_placeholder(), "Search...");
158    ///
159    /// let fr = Translations::new(Language::French);
160    /// assert_eq!(fr.search_placeholder(), "Rechercher...");
161    /// ```
162    #[must_use]
163    pub fn search_placeholder(&self) -> String {
164        rust_i18n::t!("search.placeholder", locale = self.language.to_locale())
165            .into_owned()
166    }
167
168    /// Returns the placeholder text for the replace input field.
169    ///
170    /// # Examples
171    ///
172    /// ```
173    /// use iced_code_editor::{Language, Translations};
174    ///
175    /// let es = Translations::new(Language::Spanish);
176    /// assert_eq!(es.replace_placeholder(), "Reemplazar...");
177    /// ```
178    #[must_use]
179    pub fn replace_placeholder(&self) -> String {
180        rust_i18n::t!("replace.placeholder", locale = self.language.to_locale())
181            .into_owned()
182    }
183
184    /// Returns the label text for the case sensitive checkbox.
185    ///
186    /// # Examples
187    ///
188    /// ```
189    /// use iced_code_editor::{Language, Translations};
190    ///
191    /// let fr = Translations::new(Language::French);
192    /// assert_eq!(fr.case_sensitive_label(), "Sensible à la casse");
193    /// ```
194    #[must_use]
195    pub fn case_sensitive_label(&self) -> String {
196        rust_i18n::t!(
197            "settings.case_sensitive_label",
198            locale = self.language.to_locale()
199        )
200        .into_owned()
201    }
202
203    /// Returns the tooltip text for the previous match button.
204    ///
205    /// # Examples
206    ///
207    /// ```
208    /// use iced_code_editor::{Language, Translations};
209    ///
210    /// let en = Translations::new(Language::English);
211    /// assert_eq!(en.previous_match_tooltip(), "Previous match (Shift+F3)");
212    /// ```
213    #[must_use]
214    pub fn previous_match_tooltip(&self) -> String {
215        rust_i18n::t!(
216            "search.previous_match_tooltip",
217            locale = self.language.to_locale()
218        )
219        .into_owned()
220    }
221
222    /// Returns the tooltip text for the next match button.
223    ///
224    /// # Examples
225    ///
226    /// ```
227    /// use iced_code_editor::{Language, Translations};
228    ///
229    /// let es = Translations::new(Language::Spanish);
230    /// assert_eq!(es.next_match_tooltip(), "Siguiente coincidencia (F3 / Enter)");
231    /// ```
232    #[must_use]
233    pub fn next_match_tooltip(&self) -> String {
234        rust_i18n::t!(
235            "search.next_match_tooltip",
236            locale = self.language.to_locale()
237        )
238        .into_owned()
239    }
240
241    /// Returns the tooltip text for the close search dialog button.
242    ///
243    /// # Examples
244    ///
245    /// ```
246    /// use iced_code_editor::{Language, Translations};
247    ///
248    /// let fr = Translations::new(Language::French);
249    /// assert_eq!(fr.close_search_tooltip(), "Fermer la recherche (Échap)");
250    /// ```
251    #[must_use]
252    pub fn close_search_tooltip(&self) -> String {
253        rust_i18n::t!(
254            "search.close_tooltip",
255            locale = self.language.to_locale()
256        )
257        .into_owned()
258    }
259
260    /// Returns the tooltip text for the replace current match button.
261    ///
262    /// # Examples
263    ///
264    /// ```
265    /// use iced_code_editor::{Language, Translations};
266    ///
267    /// let en = Translations::new(Language::English);
268    /// assert_eq!(en.replace_current_tooltip(), "Replace current match");
269    /// ```
270    #[must_use]
271    pub fn replace_current_tooltip(&self) -> String {
272        rust_i18n::t!(
273            "replace.current_tooltip",
274            locale = self.language.to_locale()
275        )
276        .into_owned()
277    }
278
279    /// Returns the tooltip text for the replace all matches button.
280    ///
281    /// # Examples
282    ///
283    /// ```
284    /// use iced_code_editor::{Language, Translations};
285    ///
286    /// let es = Translations::new(Language::Spanish);
287    /// assert_eq!(es.replace_all_tooltip(), "Reemplazar todo");
288    /// ```
289    #[must_use]
290    pub fn replace_all_tooltip(&self) -> String {
291        rust_i18n::t!("replace.all_tooltip", locale = self.language.to_locale())
292            .into_owned()
293    }
294
295    /// Returns the context-menu label for undo.
296    #[must_use]
297    pub fn context_menu_undo(&self) -> String {
298        rust_i18n::t!("context_menu.undo", locale = self.language.to_locale())
299            .into_owned()
300    }
301
302    /// Returns the context-menu label for redo.
303    #[must_use]
304    pub fn context_menu_redo(&self) -> String {
305        rust_i18n::t!("context_menu.redo", locale = self.language.to_locale())
306            .into_owned()
307    }
308
309    /// Returns the context-menu label for cut.
310    #[must_use]
311    pub fn context_menu_cut(&self) -> String {
312        rust_i18n::t!("context_menu.cut", locale = self.language.to_locale())
313            .into_owned()
314    }
315
316    /// Returns the context-menu label for copy.
317    #[must_use]
318    pub fn context_menu_copy(&self) -> String {
319        rust_i18n::t!("context_menu.copy", locale = self.language.to_locale())
320            .into_owned()
321    }
322
323    /// Returns the context-menu label for paste.
324    #[must_use]
325    pub fn context_menu_paste(&self) -> String {
326        rust_i18n::t!("context_menu.paste", locale = self.language.to_locale())
327            .into_owned()
328    }
329
330    /// Returns the context-menu label for select all.
331    #[must_use]
332    pub fn context_menu_select_all(&self) -> String {
333        rust_i18n::t!(
334            "context_menu.select_all",
335            locale = self.language.to_locale()
336        )
337        .into_owned()
338    }
339
340    /// Returns the macOS context-menu label for revealing a file.
341    #[must_use]
342    pub fn context_menu_reveal_in_finder(&self) -> String {
343        rust_i18n::t!(
344            "context_menu.reveal_in_finder",
345            locale = self.language.to_locale()
346        )
347        .into_owned()
348    }
349
350    /// Returns the Windows context-menu label for revealing a file.
351    #[must_use]
352    pub fn context_menu_reveal_in_file_explorer(&self) -> String {
353        rust_i18n::t!(
354            "context_menu.reveal_in_file_explorer",
355            locale = self.language.to_locale()
356        )
357        .into_owned()
358    }
359
360    /// Returns the Linux context-menu label for opening a file's parent folder.
361    #[must_use]
362    pub fn context_menu_open_containing_folder(&self) -> String {
363        rust_i18n::t!(
364            "context_menu.open_containing_folder",
365            locale = self.language.to_locale()
366        )
367        .into_owned()
368    }
369
370    /// Returns the platform-appropriate label for revealing a file.
371    #[must_use]
372    pub fn context_menu_reveal_in_file_manager(&self) -> String {
373        if cfg!(target_os = "macos") {
374            self.context_menu_reveal_in_finder()
375        } else if cfg!(target_os = "windows") {
376            self.context_menu_reveal_in_file_explorer()
377        } else {
378            self.context_menu_open_containing_folder()
379        }
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386
387    #[test]
388    fn test_default_language() {
389        let translations = Translations::default();
390        assert_eq!(translations.language(), Language::English);
391    }
392
393    #[test]
394    fn test_new_with_language() {
395        let translations = Translations::new(Language::French);
396        assert_eq!(translations.language(), Language::French);
397    }
398
399    #[test]
400    fn test_set_language() {
401        let mut translations = Translations::new(Language::English);
402        translations.set_language(Language::Spanish);
403        assert_eq!(translations.language(), Language::Spanish);
404    }
405
406    #[test]
407    fn test_english_translations() {
408        let t = Translations::new(Language::English);
409        assert_eq!(t.search_placeholder(), "Search...");
410        assert_eq!(t.replace_placeholder(), "Replace...");
411        assert_eq!(t.case_sensitive_label(), "Case sensitive");
412        assert_eq!(t.previous_match_tooltip(), "Previous match (Shift+F3)");
413        assert_eq!(t.next_match_tooltip(), "Next match (F3 / Enter)");
414        assert_eq!(t.close_search_tooltip(), "Close search dialog (Esc)");
415        assert_eq!(t.replace_current_tooltip(), "Replace current match");
416        assert_eq!(t.replace_all_tooltip(), "Replace all matches");
417    }
418
419    #[test]
420    fn test_french_translations() {
421        let t = Translations::new(Language::French);
422        assert_eq!(t.search_placeholder(), "Rechercher...");
423        assert_eq!(t.replace_placeholder(), "Remplacer...");
424        assert_eq!(t.case_sensitive_label(), "Sensible à la casse");
425        assert_eq!(t.previous_match_tooltip(), "Résultat précédent (Maj+F3)");
426        assert_eq!(t.next_match_tooltip(), "Résultat suivant (F3 / Entrée)");
427        assert_eq!(t.close_search_tooltip(), "Fermer la recherche (Échap)");
428        assert_eq!(
429            t.replace_current_tooltip(),
430            "Remplacer l'occurrence actuelle"
431        );
432        assert_eq!(t.replace_all_tooltip(), "Tout remplacer");
433    }
434
435    #[test]
436    fn test_spanish_translations() {
437        let t = Translations::new(Language::Spanish);
438        assert_eq!(t.search_placeholder(), "Buscar...");
439        assert_eq!(t.replace_placeholder(), "Reemplazar...");
440        assert_eq!(t.case_sensitive_label(), "Distinguir mayúsculas");
441        assert_eq!(
442            t.previous_match_tooltip(),
443            "Coincidencia anterior (Mayús+F3)"
444        );
445        assert_eq!(
446            t.next_match_tooltip(),
447            "Siguiente coincidencia (F3 / Enter)"
448        );
449        assert_eq!(t.close_search_tooltip(), "Cerrar búsqueda (Esc)");
450        assert_eq!(
451            t.replace_current_tooltip(),
452            "Reemplazar coincidencia actual"
453        );
454        assert_eq!(t.replace_all_tooltip(), "Reemplazar todo");
455    }
456
457    #[test]
458    fn test_german_translations() {
459        let t = Translations::new(Language::German);
460        assert_eq!(t.search_placeholder(), "Suchen...");
461        assert_eq!(t.replace_placeholder(), "Ersetzen...");
462        assert_eq!(t.case_sensitive_label(), "Groß-/Kleinschreibung");
463        assert_eq!(
464            t.previous_match_tooltip(),
465            "Vorheriger Treffer (Umschalt+F3)"
466        );
467        assert_eq!(t.next_match_tooltip(), "Nächster Treffer (F3 / Enter)");
468        assert_eq!(t.close_search_tooltip(), "Suchdialog schließen (Esc)");
469        assert_eq!(t.replace_current_tooltip(), "Aktuellen Treffer ersetzen");
470        assert_eq!(t.replace_all_tooltip(), "Alle ersetzen");
471    }
472
473    #[test]
474    fn test_italian_translations() {
475        let t = Translations::new(Language::Italian);
476        assert_eq!(t.search_placeholder(), "Cerca...");
477        assert_eq!(t.replace_placeholder(), "Sostituisci...");
478        assert_eq!(t.case_sensitive_label(), "Distingui maiuscole");
479        assert_eq!(
480            t.previous_match_tooltip(),
481            "Risultato precedente (Maiusc+F3)"
482        );
483        assert_eq!(t.next_match_tooltip(), "Risultato successivo (F3 / Invio)");
484        assert_eq!(
485            t.close_search_tooltip(),
486            "Chiudi finestra di ricerca (Esc)"
487        );
488        assert_eq!(
489            t.replace_current_tooltip(),
490            "Sostituisci risultato corrente"
491        );
492        assert_eq!(t.replace_all_tooltip(), "Sostituisci tutto");
493    }
494
495    #[test]
496    fn test_portuguese_br_translations() {
497        let t = Translations::new(Language::PortugueseBR);
498        assert_eq!(t.search_placeholder(), "Pesquisar...");
499        assert_eq!(t.replace_placeholder(), "Substituir...");
500        assert_eq!(t.case_sensitive_label(), "Diferenciar maiúsculas");
501        assert_eq!(
502            t.previous_match_tooltip(),
503            "Correspondência anterior (Shift+F3)"
504        );
505        assert_eq!(
506            t.next_match_tooltip(),
507            "Próxima correspondência (F3 / Enter)"
508        );
509        assert_eq!(
510            t.close_search_tooltip(),
511            "Fechar diálogo de pesquisa (Esc)"
512        );
513        assert_eq!(
514            t.replace_current_tooltip(),
515            "Substituir correspondência atual"
516        );
517        assert_eq!(t.replace_all_tooltip(), "Substituir tudo");
518    }
519
520    #[test]
521    fn test_portuguese_pt_translations() {
522        let t = Translations::new(Language::PortuguesePT);
523        assert_eq!(t.search_placeholder(), "Pesquisar...");
524        assert_eq!(t.replace_placeholder(), "Substituir...");
525        assert_eq!(t.case_sensitive_label(), "Diferenciar maiúsculas");
526        assert_eq!(
527            t.previous_match_tooltip(),
528            "Correspondência anterior (Shift+F3)"
529        );
530        assert_eq!(
531            t.next_match_tooltip(),
532            "Próxima correspondência (F3 / Enter)"
533        );
534        assert_eq!(
535            t.close_search_tooltip(),
536            "Fechar diálogo de pesquisa (Esc)"
537        );
538        assert_eq!(
539            t.replace_current_tooltip(),
540            "Substituir correspondência actual"
541        );
542        assert_eq!(t.replace_all_tooltip(), "Substituir tudo");
543    }
544
545    #[test]
546    fn test_language_switching() {
547        let mut t = Translations::new(Language::English);
548        assert_eq!(t.search_placeholder(), "Search...");
549
550        t.set_language(Language::French);
551        assert_eq!(t.search_placeholder(), "Rechercher...");
552
553        t.set_language(Language::Spanish);
554        assert_eq!(t.search_placeholder(), "Buscar...");
555
556        t.set_language(Language::German);
557        assert_eq!(t.search_placeholder(), "Suchen...");
558
559        t.set_language(Language::Italian);
560        assert_eq!(t.search_placeholder(), "Cerca...");
561
562        t.set_language(Language::PortugueseBR);
563        assert_eq!(t.search_placeholder(), "Pesquisar...");
564
565        t.set_language(Language::PortuguesePT);
566        assert_eq!(t.search_placeholder(), "Pesquisar...");
567
568        t.set_language(Language::ChineseSimplified);
569        assert_eq!(t.search_placeholder(), "搜索...");
570    }
571
572    #[test]
573    fn test_context_menu_translations_cover_all_locales() {
574        let cases = [
575            (
576                Language::English,
577                [
578                    "Undo",
579                    "Redo",
580                    "Cut",
581                    "Copy",
582                    "Paste",
583                    "Select All",
584                    "Reveal in Finder",
585                    "Reveal in File Explorer",
586                    "Open Containing Folder",
587                ],
588            ),
589            (
590                Language::French,
591                [
592                    "Annuler",
593                    "Rétablir",
594                    "Couper",
595                    "Copier",
596                    "Coller",
597                    "Tout sélectionner",
598                    "Révéler dans le Finder",
599                    "Afficher dans l'Explorateur de fichiers",
600                    "Ouvrir le dossier contenant",
601                ],
602            ),
603            (
604                Language::Spanish,
605                [
606                    "Deshacer",
607                    "Rehacer",
608                    "Cortar",
609                    "Copiar",
610                    "Pegar",
611                    "Seleccionar todo",
612                    "Mostrar en Finder",
613                    "Mostrar en el Explorador de archivos",
614                    "Abrir carpeta contenedora",
615                ],
616            ),
617            (
618                Language::German,
619                [
620                    "Rückgängig",
621                    "Wiederholen",
622                    "Ausschneiden",
623                    "Kopieren",
624                    "Einfügen",
625                    "Alle auswählen",
626                    "Im Finder anzeigen",
627                    "Im Datei-Explorer anzeigen",
628                    "Übergeordneten Ordner öffnen",
629                ],
630            ),
631            (
632                Language::Italian,
633                [
634                    "Annulla azione",
635                    "Ripeti",
636                    "Taglia",
637                    "Copia",
638                    "Incolla",
639                    "Seleziona tutto",
640                    "Visualizza in Finder",
641                    "Visualizza in Esplora file",
642                    "Apri cartella superiore",
643                ],
644            ),
645            (
646                Language::PortugueseBR,
647                [
648                    "Desfazer",
649                    "Refazer",
650                    "Recortar",
651                    "Copiar",
652                    "Colar",
653                    "Selecionar Tudo",
654                    "Revelar no Finder",
655                    "Revelar no Explorador de Arquivos",
656                    "Abrir a Pasta Que Contém",
657                ],
658            ),
659            (
660                Language::PortuguesePT,
661                [
662                    "Anular",
663                    "Refazer",
664                    "Cortar",
665                    "Copiar",
666                    "Colar",
667                    "Selecionar tudo",
668                    "Mostrar no Finder",
669                    "Mostrar no Explorador de Ficheiros",
670                    "Abrir pasta contentora",
671                ],
672            ),
673            (
674                Language::ChineseSimplified,
675                [
676                    "撤消",
677                    "恢复",
678                    "剪切",
679                    "复制",
680                    "粘贴",
681                    "选择全部",
682                    "在访达中显示",
683                    "在文件资源管理器中显示",
684                    "打开所在的文件夹",
685                ],
686            ),
687        ];
688
689        for (language, expected) in cases {
690            let translations = Translations::new(language);
691            assert_eq!(translations.context_menu_undo(), expected[0]);
692            assert_eq!(translations.context_menu_redo(), expected[1]);
693            assert_eq!(translations.context_menu_cut(), expected[2]);
694            assert_eq!(translations.context_menu_copy(), expected[3]);
695            assert_eq!(translations.context_menu_paste(), expected[4]);
696            assert_eq!(translations.context_menu_select_all(), expected[5]);
697            assert_eq!(
698                translations.context_menu_reveal_in_finder(),
699                expected[6]
700            );
701            assert_eq!(
702                translations.context_menu_reveal_in_file_explorer(),
703                expected[7]
704            );
705            assert_eq!(
706                translations.context_menu_open_containing_folder(),
707                expected[8]
708            );
709        }
710    }
711}