poise_i18n/
lib.rs

1use std::hash::Hash;
2/// This is kind of a disgusting type magic, but i think it is kind of understable
3/// given the problem that this is trying to solve.
4use std::{collections::HashMap, fmt::Display, str::FromStr};
5
6use bevy_reflect::Reflect;
7use itertools::{iproduct, Itertools};
8use poise::{Command, CommandParameter, CommandParameterChoice};
9use rusty18n::{I18NAccess, I18NFallback, I18NReflected, I18NTrait, I18NWrapper, R};
10use strum::{Display, EnumIter, IntoEnumIterator};
11
12pub trait PoiseI18NMeta<
13    'a,
14    K: Eq + Hash + Default + Copy + ToString + FromStr + Display,
15    V: I18NFallback,
16>
17{
18    // Returns references to the required locales.
19    fn locales(&self) -> &'a I18NWrapper<K, V>;
20}
21
22/// Automatically implemented trait for contexts that provide locales.
23pub trait PoiseI18NTrait<
24    'a,
25    K: Eq + Hash + Default + Copy + ToString + FromStr + Display,
26    V: I18NFallback,
27>
28{
29    // Acquires i18n access.
30    fn i18n(&'a self) -> I18NAccess<I18NWrapper<K, V>>;
31    fn i18n_explicit(&'a self, wrapper: &'a I18NWrapper<K, V>)
32        -> I18NAccess<'a, I18NWrapper<K, V>>;
33}
34
35impl<
36        'a,
37        K: Eq + Hash + Default + Copy + ToString + FromStr + Display + 'a,
38        V: I18NFallback,
39        U,
40        E,
41    > PoiseI18NTrait<'a, K, V> for poise::Context<'a, U, E>
42where
43    Self: PoiseI18NMeta<'a, K, V>,
44{
45    fn i18n(&'a self) -> I18NAccess<I18NWrapper<K, V>> {
46        self.i18n_explicit(self.locales())
47    }
48
49    fn i18n_explicit(
50        &'a self,
51        wrapper: &'a I18NWrapper<K, V>,
52    ) -> I18NAccess<'a, I18NWrapper<K, V>> {
53        wrapper.get(K::from_str(self.locale().unwrap_or_default()).unwrap_or_default())
54    }
55}
56
57#[derive(Display, EnumIter, Clone)]
58#[strum(serialize_all = "snake_case")]
59enum CommandLocalization {
60    Name,
61    Description,
62}
63
64struct I18NAccesses<'a, L: I18NTrait>(Vec<(String, I18NAccess<'a, L>)>);
65
66pub fn apply_translations<
67    K: Eq + Hash + Default + Copy + ToString + FromStr + Display,
68    V: I18NFallback + Reflect,
69    U,
70    E,
71>(
72    commands: &mut [Command<U, E>],
73    wrapper: &I18NWrapper<K, V>,
74) {
75    apply_translation(
76        commands,
77        &I18NAccesses(
78            wrapper
79                .store
80                .0
81                .keys()
82                .map(|key| (key.to_string(), wrapper.get(*key)))
83                .collect_vec(),
84        ),
85    )
86}
87
88trait PoiseI18NLocalizable {
89    fn name_localizations(&mut self) -> &mut HashMap<String, String>;
90    fn description_localizations(&mut self) -> Option<&mut HashMap<String, String>>;
91}
92
93macro_rules! impl_localizable {
94    ($struct:ident) => {
95        impl<U, E> PoiseI18NLocalizable for $struct<U, E> {
96            fn name_localizations(&mut self) -> &mut HashMap<String, String> {
97                &mut self.name_localizations
98            }
99
100            fn description_localizations(&mut self) -> Option<&mut HashMap<String, String>> {
101                Some(&mut self.description_localizations)
102            }
103        }
104    };
105}
106
107impl_localizable!(Command);
108impl_localizable!(CommandParameter);
109
110impl PoiseI18NLocalizable for CommandParameterChoice {
111    fn name_localizations(&mut self) -> &mut HashMap<String, String> {
112        &mut self.localizations
113    }
114
115    fn description_localizations(&mut self) -> Option<&mut HashMap<String, String>> {
116        None
117    }
118}
119
120fn apply_localization<L: I18NTrait>(
121    path: &mut Vec<String>,
122    next_tag: String,
123    localizable: &mut impl PoiseI18NLocalizable,
124    locale_accesses: &I18NAccesses<'_, L>,
125) where
126    L::K: Display,
127    L::V: Reflect,
128{
129    path.push(next_tag);
130
131    let locale_tags = CommandLocalization::iter()
132        .map(|l| {
133            let mut path_new = path.clone();
134
135            path_new.push(l.to_string());
136
137            let path_string = path.iter().join(".");
138
139            (l, path_string)
140        })
141        .collect_vec();
142
143    // All combinations of locale acesses and locale tags that can
144    // be used for this command.
145    let permutations = iproduct!(&locale_accesses.0, &locale_tags);
146
147    for ((lang_key, access), (locale_type, tag)) in permutations {
148        let possible_resource = access.by_path::<R>(tag);
149
150        let Some(localized_key) = possible_resource else {
151            continue;
152        };
153
154        let lang_key = lang_key.clone();
155        let localized_key = localized_key.clone();
156
157        match locale_type {
158            CommandLocalization::Name => {
159                localizable
160                    .name_localizations()
161                    .insert(lang_key, localized_key);
162            }
163            CommandLocalization::Description => {
164                match localizable.description_localizations() {
165                    Some(v) => v.insert(lang_key, localized_key),
166                    None => {
167                        continue;
168                    }
169                };
170            }
171        };
172    }
173}
174
175fn apply_translation<L: I18NTrait, U, E>(
176    commands: &mut [Command<U, E>],
177    locale_accesses: &I18NAccesses<L>,
178) where
179    L::K: Display,
180    L::V: Reflect,
181{
182    for command in commands {
183        let mut path_vec = vec![];
184
185        // Recursive case to apply on subcommands too.
186        apply_translation(&mut command.subcommands, locale_accesses);
187
188        // This could be recursive, we could have a trait that defines Children.
189        // and we keep calling apply_localization to all the children of the
190        // children of the child... Yeah, you get it.
191        apply_localization(
192            &mut path_vec,
193            command.name.clone(),
194            command,
195            locale_accesses,
196        );
197
198        for param in &mut command.parameters {
199            apply_localization(&mut path_vec, param.name.clone(), param, locale_accesses);
200
201            for choice in &mut param.choices {
202                apply_localization(&mut path_vec, choice.name.clone(), choice, locale_accesses)
203            }
204        }
205    }
206}