langcodec_cli/
formats.rs

1use std::str::FromStr;
2
3/// Custom format types that are not supported by the lib crate.
4/// These are one-way conversions only (to Resource format).
5#[derive(Debug, Clone, PartialEq, clap::ValueEnum)]
6#[allow(clippy::enum_variant_names)]
7pub enum CustomFormat {
8    /// A JSON file which contains a map of language codes to translations.
9    ///
10    /// The key is the localization code, and the value is the translation:
11    ///
12    /// ```json
13    /// {
14    ///     "key": "hello_world",
15    ///     "en": "Hello, World!",
16    ///     "fr": "Bonjour, le monde!"
17    /// }
18    /// ```
19    JSONLanguageMap,
20
21    /// A YAML file which contains a map of language codes to translations.
22    ///
23    /// The key is the localization code, and the value is the translation:
24    ///
25    /// ```yaml
26    /// key: hello_world
27    /// en: Hello, World!
28    /// fr: Bonjour, le monde!
29    /// ```
30    YAMLLanguageMap,
31
32    /// A JSON file which contains an array of language map objects.
33    ///
34    /// Each object contains a key and translations for different languages:
35    ///
36    /// ```json
37    /// [
38    ///     {
39    ///         "key": "hello_world",
40    ///         "en": "Hello, World!",
41    ///         "fr": "Bonjour, le monde!"
42    ///     },
43    ///     {
44    ///         "key": "welcome_message",
45    ///         "en": "Welcome to our app!",
46    ///         "fr": "Bienvenue dans notre application!"
47    ///     }
48    /// ]
49    /// ```
50    JSONArrayLanguageMap,
51
52    /// A JSON file which contains an array of langcodec::Resource objects.
53    ///
54    /// Each object is a complete Resource with metadata and entries:
55    ///
56    /// ```json
57    /// [
58    ///     {
59    ///         "metadata": {
60    ///             "language": "en",
61    ///             "domain": "MyApp"
62    ///         },
63    ///         "entries": [
64    ///             {
65    ///                 "id": "hello_world",
66    ///                 "value": "Hello, World!",
67    ///                 "comment": "Welcome message"
68    ///             }
69    ///         ]
70    ///     },
71    ///     {
72    ///         "metadata": {
73    ///             "language": "fr",
74    ///             "domain": "MyApp"
75    ///         },
76    ///         "entries": [
77    ///             {
78    ///                 "id": "hello_world",
79    ///                 "value": "Bonjour, le monde!",
80    ///                 "comment": "Welcome message"
81    ///             }
82    ///         ]
83    ///     }
84    /// ]
85    /// ```
86    LangcodecResourceArray,
87}
88
89impl FromStr for CustomFormat {
90    type Err = String;
91
92    fn from_str(s: &str) -> Result<Self, Self::Err> {
93        let normalized = s.trim().to_ascii_lowercase().replace(['-', '_'], "");
94        //: cspell:disable
95        match normalized.as_str() {
96            "jsonlanguagemap" => Ok(CustomFormat::JSONLanguageMap),
97            "jsonarraylanguagemap" => Ok(CustomFormat::JSONArrayLanguageMap),
98            "yamllanguagemap" => Ok(CustomFormat::YAMLLanguageMap),
99            "langcodecresourcearray" => Ok(CustomFormat::LangcodecResourceArray),
100            // "csvlanguages" => Ok(CustomFormat::CSVLanguages),
101            _ => Err(format!(
102                "Unknown custom format: '{}'. Supported formats: json-language-map, json-array-language-map, yaml-language-map, langcodec-resource-array",
103                s
104            )),
105        }
106        //: cspell:enable
107    }
108}
109
110/// Parse a custom format from a string, with helpful error messages.
111pub fn parse_custom_format(s: &str) -> Result<CustomFormat, String> {
112    CustomFormat::from_str(s)
113}
114
115/// Detect if a file is a custom format based on its content and extension.
116/// Returns the detected custom format if found, None otherwise.
117pub fn detect_custom_format(file_path: &str, file_content: &str) -> Option<CustomFormat> {
118    let extension = std::path::Path::new(file_path)
119        .extension()
120        .and_then(|ext| ext.to_str())
121        .unwrap_or("")
122        .to_lowercase();
123
124    match extension.as_str() {
125        "langcodec" => {
126            // Try to parse as JSON array of Resource objects
127            if serde_json::from_str::<Vec<serde_json::Value>>(file_content).is_ok() {
128                // Check if it looks like an array of Resource objects
129                if let Ok(array) = serde_json::from_str::<Vec<serde_json::Value>>(file_content)
130                    && !array.is_empty()
131                {
132                    // Check if the first element has the expected Resource structure
133                    if let Some(first) = array.first()
134                        && let Some(obj) = first.as_object()
135                        && obj.contains_key("metadata")
136                        && obj.contains_key("entries")
137                    {
138                        return Some(CustomFormat::LangcodecResourceArray);
139                    }
140                }
141            }
142        }
143        "json" => {
144            // Try to parse as JSON object first (JSONLanguageMap)
145            if serde_json::from_str::<serde_json::Value>(file_content).is_ok() {
146                // Check if it's an object (not an array)
147                if let Ok(obj) = serde_json::from_str::<
148                    std::collections::HashMap<String, serde_json::Value>,
149                >(file_content)
150                    && !obj.is_empty()
151                {
152                    return Some(CustomFormat::JSONLanguageMap);
153                }
154                // Check if it's an array (JSONArrayLanguageMap)
155                if serde_json::from_str::<Vec<serde_json::Value>>(file_content).is_ok() {
156                    return Some(CustomFormat::JSONArrayLanguageMap);
157                }
158            }
159        }
160        "yaml" | "yml" => {
161            // Try to parse as YAML
162            if serde_yaml::from_str::<serde_yaml::Value>(file_content).is_ok() {
163                return Some(CustomFormat::YAMLLanguageMap);
164            }
165        }
166        _ => {}
167    }
168
169    None
170}
171
172/// Validate custom format file content
173pub fn validate_custom_format_content(
174    file_path: &str,
175    file_content: &str,
176) -> Result<CustomFormat, String> {
177    if file_content.trim().is_empty() {
178        return Err("File content is empty".to_string());
179    }
180
181    if let Some(format) = detect_custom_format(file_path, file_content) {
182        Ok(format)
183    } else {
184        Err(format!(
185            "Could not detect custom format from file content. Supported formats: {}",
186            get_supported_custom_formats()
187        ))
188    }
189}
190
191/// Get a list of all supported custom formats for help messages.
192pub fn get_supported_custom_formats() -> &'static str {
193    "json-language-map, json-array-language-map, yaml-language-map, langcodec-resource-array"
194}