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" if is_langcodec_resource_array(file_content) => {
126 Some(CustomFormat::LangcodecResourceArray)
127 }
128 "json" if is_json_language_map(file_content) => Some(CustomFormat::JSONLanguageMap),
129 "json" if serde_json::from_str::<Vec<serde_json::Value>>(file_content).is_ok() => {
130 Some(CustomFormat::JSONArrayLanguageMap)
131 }
132 "yaml" | "yml" if serde_yaml::from_str::<serde_yaml::Value>(file_content).is_ok() => {
133 Some(CustomFormat::YAMLLanguageMap)
134 }
135 _ => None,
136 }
137}
138
139fn is_langcodec_resource_array(file_content: &str) -> bool {
140 let Ok(array) = serde_json::from_str::<Vec<serde_json::Value>>(file_content) else {
141 return false;
142 };
143 let Some(first) = array.first() else {
144 return false;
145 };
146
147 first
148 .as_object()
149 .is_some_and(|obj| obj.contains_key("metadata") && obj.contains_key("entries"))
150}
151
152fn is_json_language_map(file_content: &str) -> bool {
153 serde_json::from_str::<std::collections::HashMap<String, serde_json::Value>>(file_content)
154 .is_ok_and(|obj| !obj.is_empty())
155}
156
157/// Validate custom format file content
158pub fn validate_custom_format_content(
159 file_path: &str,
160 file_content: &str,
161) -> Result<CustomFormat, String> {
162 if file_content.trim().is_empty() {
163 return Err("File content is empty".to_string());
164 }
165
166 if let Some(format) = detect_custom_format(file_path, file_content) {
167 Ok(format)
168 } else {
169 Err(format!(
170 "Could not detect custom format from file content. Supported formats: {}",
171 get_supported_custom_formats()
172 ))
173 }
174}
175
176/// Get a list of all supported custom formats for help messages.
177pub fn get_supported_custom_formats() -> &'static str {
178 "json-language-map, json-array-language-map, yaml-language-map, langcodec-resource-array"
179}