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