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}