rust_i18n_extract/
extractor.rs

1use anyhow::Error;
2use proc_macro2::{TokenStream, TokenTree};
3use quote::ToTokens;
4use rust_i18n_support::I18nConfig;
5use std::collections::HashMap;
6use std::path::PathBuf;
7
8pub type Results = HashMap<String, Message>;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Location {
12    pub file: std::path::PathBuf,
13    pub line: usize,
14}
15
16#[derive(Debug, Clone, Default, PartialEq, Eq)]
17pub struct Message {
18    pub key: String,
19    pub index: usize,
20    pub minify_key: bool,
21    pub locations: Vec<Location>,
22}
23
24impl Message {
25    fn new(key: &str, index: usize, minify_key: bool) -> Self {
26        Self {
27            key: key.to_owned(),
28            index,
29            minify_key,
30            locations: vec![],
31        }
32    }
33}
34
35static METHOD_NAMES: &[&str] = &["t", "tr"];
36
37#[allow(clippy::ptr_arg)]
38pub fn extract(
39    results: &mut Results,
40    path: &PathBuf,
41    source: &str,
42    cfg: I18nConfig,
43) -> Result<(), Error> {
44    let mut ex = Extractor { results, path, cfg };
45
46    let file = syn::parse_file(source)
47        .unwrap_or_else(|_| panic!("Failed to parse file, file: {}", path.display()));
48    let stream = file.into_token_stream();
49    ex.invoke(stream)
50}
51
52#[allow(dead_code)]
53struct Extractor<'a> {
54    results: &'a mut Results,
55    path: &'a PathBuf,
56    cfg: I18nConfig,
57}
58
59impl<'a> Extractor<'a> {
60    fn invoke(&mut self, stream: TokenStream) -> Result<(), Error> {
61        let mut token_iter = stream.into_iter().peekable();
62
63        while let Some(token) = token_iter.next() {
64            match token {
65                TokenTree::Group(group) => self.invoke(group.stream())?,
66                TokenTree::Ident(ident) => {
67                    let mut is_macro = false;
68                    if let Some(TokenTree::Punct(punct)) = token_iter.peek() {
69                        if punct.to_string() == "!" {
70                            is_macro = true;
71                            token_iter.next();
72                        }
73                    }
74
75                    let ident_str = ident.to_string();
76                    if METHOD_NAMES.contains(&ident_str.as_str()) && is_macro {
77                        if let Some(TokenTree::Group(group)) = token_iter.peek() {
78                            self.take_message(group.stream());
79                        }
80                    }
81                }
82                _ => {}
83            }
84        }
85
86        Ok(())
87    }
88
89    fn take_message(&mut self, stream: TokenStream) {
90        let mut token_iter = stream.into_iter().peekable();
91
92        let literal = if let Some(TokenTree::Literal(literal)) = token_iter.next() {
93            literal
94        } else {
95            return;
96        };
97
98        let I18nConfig {
99            minify_key,
100            minify_key_len,
101            minify_key_prefix,
102            minify_key_thresh,
103            ..
104        } = &self.cfg;
105        let key: Option<proc_macro2::Literal> = Some(literal);
106
107        if let Some(lit) = key {
108            if let Some(key) = literal_to_string(&lit) {
109                let (message_key, message_content) = if *minify_key {
110                    let hashed_key = rust_i18n_support::MinifyKey::minify_key(
111                        &key,
112                        *minify_key_len,
113                        minify_key_prefix,
114                        *minify_key_thresh,
115                    );
116                    (hashed_key.to_string(), key.clone())
117                } else {
118                    let message_key = format_message_key(&key);
119                    (message_key.clone(), message_key)
120                };
121                let index = self.results.len();
122                let message = self
123                    .results
124                    .entry(message_key)
125                    .or_insert_with(|| Message::new(&message_content, index, *minify_key));
126
127                let span = lit.span();
128                let line = span.start().line;
129                if line > 0 {
130                    message.locations.push(Location {
131                        file: self.path.clone(),
132                        line,
133                    });
134                }
135            }
136        }
137    }
138}
139
140fn literal_to_string(lit: &proc_macro2::Literal) -> Option<String> {
141    match syn::parse_str::<syn::LitStr>(&lit.to_string()) {
142        Ok(lit) => Some(lit.value()),
143        Err(_) => None,
144    }
145}
146
147fn format_message_key(key: &str) -> String {
148    let re = regex::Regex::new(r"\s+").unwrap();
149    let key = re.replace_all(key, " ").into_owned();
150    key.trim().into()
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use std::str::FromStr;
157
158    macro_rules! build_messages {
159        {$(($key:tt, $($line:tt),+)),+} => {{
160            let mut results = Vec::<Message>::new();
161            $(
162                let message = Message {
163                    key: $key.into(),
164                    locations: vec![
165                        $(
166                            Location {
167                                file: PathBuf::from_str("hello.rs").unwrap(),
168                                line: $line
169                            },
170                        )+
171                    ],
172                    index: 0,
173                    minify_key: false,
174                };
175                results.push(message);
176            )+
177
178            results
179        }}
180    }
181
182    #[test]
183    fn test_format_message_key() {
184        assert_eq!(format_message_key("Hello world"), "Hello world".to_owned());
185        assert_eq!(format_message_key("\n    "), "".to_owned());
186        assert_eq!(format_message_key("\n    "), "".to_owned());
187        assert_eq!(format_message_key("\n    hello"), "hello".to_owned());
188        assert_eq!(format_message_key("\n    hello\n"), "hello".to_owned());
189        assert_eq!(format_message_key("\n    hello\n    "), "hello".to_owned());
190        assert_eq!(
191            format_message_key("\n    hello\n    world"),
192            "hello world".to_owned()
193        );
194        assert_eq!(
195            format_message_key("\n    hello\n    world\n\n"),
196            "hello world".to_owned()
197        );
198        assert_eq!(
199            format_message_key("\n    hello\n    world\n    "),
200            "hello world".to_owned()
201        );
202        assert_eq!(
203            format_message_key("    hello\n    world\n    "),
204            "hello world".to_owned()
205        );
206        assert_eq!(
207            format_message_key(
208                r#"Use YAML for mapping localized text, 
209            and support mutiple YAML files merging."#
210            ),
211            "Use YAML for mapping localized text, and support mutiple YAML files merging."
212                .to_owned()
213        );
214    }
215
216    #[test]
217    fn test_extract() {
218        let source = include_str!("example.test.rs");
219        let stream = proc_macro2::TokenStream::from_str(source).unwrap();
220
221        let expected = build_messages![
222            ("hello", 4),
223            ("views.message.title", 5),
224            ("views.message.description", 7),
225            (
226                "Use YAML for mapping localized text, and support mutiple YAML files merging.",
227                11,
228                14
229            ),
230            (
231                "The table below describes some of those behaviours.",
232                18,
233                20
234            )
235        ];
236
237        let mut results = HashMap::new();
238
239        let mut ex = Extractor {
240            results: &mut results,
241            path: &"hello.rs".to_owned().into(),
242            cfg: I18nConfig::default(),
243        };
244
245        ex.invoke(stream).unwrap();
246
247        let mut messages: Vec<_> = ex.results.values().collect();
248        messages.sort_by_key(|m| m.index);
249        assert_eq!(expected.len(), messages.len());
250
251        for (expected_message, actually_message) in expected.iter().zip(messages) {
252            let mut actually_message = actually_message.clone();
253            actually_message.index = 0;
254
255            assert_eq!(*expected_message, actually_message);
256        }
257    }
258}