1#![allow(unused)]
8
9use miette::{Diagnostic, NamedSource, SourceSpan};
10use std::path::PathBuf;
11use thiserror::Error;
12
13#[derive(Debug, Diagnostic, Error)]
15#[error("i18n.toml configuration file not found")]
16#[diagnostic(
17 code(es_fluent::config::not_found),
18 help(
19 "Create an i18n.toml file in your crate root with the following content:\n\n \
20 fallback_language = \"en\"\n \
21 assets_dir = \"i18n\"\n"
22 )
23)]
24pub struct ConfigNotFoundError {
25 pub expected_path: PathBuf,
27}
28
29#[derive(Debug, Diagnostic, Error)]
31#[error("failed to parse i18n.toml configuration")]
32#[diagnostic(code(es_fluent::config::parse_error))]
33pub struct ConfigParseError {
34 #[source_code]
36 pub src: NamedSource<String>,
37
38 #[label("error occurred here")]
40 pub span: Option<SourceSpan>,
41
42 #[help]
44 pub help: String,
45}
46
47#[derive(Debug, Diagnostic, Error)]
49#[error("assets directory not found: {path}")]
50#[diagnostic(
51 code(es_fluent::config::assets_not_found),
52 help("Create the assets directory or update assets_dir in i18n.toml")
53)]
54pub struct AssetsNotFoundError {
55 pub path: PathBuf,
57}
58
59#[derive(Debug, Diagnostic, Error)]
61#[error("fallback language directory not found: {language}")]
62#[diagnostic(
63 code(es_fluent::config::fallback_not_found),
64 help("Create a directory named '{language}' in your assets folder")
65)]
66pub struct FallbackLanguageNotFoundError {
67 pub language: String,
69}
70
71#[derive(Debug, Diagnostic, Error)]
73#[error("invalid language identifier: {identifier}")]
74#[diagnostic(
75 code(es_fluent::config::invalid_language),
76 help("Use a valid BCP 47 language tag (e.g., 'en', 'en-US', 'zh-Hans')")
77)]
78pub struct InvalidLanguageError {
79 pub identifier: String,
81}
82
83#[derive(Debug, Diagnostic, Error)]
85#[error("locale '{locale}' not found")]
86#[diagnostic(
87 code(es_fluent::config::locale_not_found),
88 help("Available locales: {available}")
89)]
90pub struct LocaleNotFoundError {
91 pub locale: String,
93 pub available: String,
95}
96
97#[derive(Debug, Diagnostic, Error)]
99#[error("missing translation key")]
100#[diagnostic(code(es_fluent::validate::missing_key), severity(Error))]
101pub struct MissingKeyError {
102 #[source_code]
104 pub src: NamedSource<String>,
105
106 pub key: String,
108
109 pub locale: String,
111
112 #[help]
114 pub help: String,
115}
116
117#[derive(Debug, Diagnostic, Error)]
119#[error("translation omits variable")]
120#[diagnostic(code(es_fluent::validate::missing_variable), severity(Warning))]
121pub struct MissingVariableWarning {
122 #[source_code]
124 pub src: NamedSource<String>,
125
126 #[label("this message omits variable '${variable}'")]
128 pub span: SourceSpan,
129
130 pub variable: String,
132
133 pub key: String,
135
136 pub locale: String,
138
139 #[help]
141 pub help: String,
142}
143
144#[derive(Debug, Diagnostic, Error)]
146#[error("FTL syntax error in {locale}/{file_name}")]
147#[diagnostic(code(es_fluent::validate::syntax_error))]
148pub struct FtlSyntaxError {
149 #[source_code]
151 pub src: NamedSource<String>,
152
153 #[label("syntax error here")]
155 pub span: SourceSpan,
156
157 pub locale: String,
159
160 pub file_name: String,
162
163 #[help]
165 pub help: String,
166}
167
168#[derive(Debug, Diagnostic, Error)]
170#[error("validation found {error_count} error(s) and {warning_count} warning(s)")]
171#[diagnostic(code(es_fluent::validate::report))]
172pub struct ValidationReport {
173 pub error_count: usize,
175
176 pub warning_count: usize,
178
179 #[related]
181 pub issues: Vec<ValidationIssue>,
182}
183
184#[derive(Debug, Diagnostic, Error)]
186pub enum ValidationIssue {
187 #[error(transparent)]
188 #[diagnostic(transparent)]
189 MissingKey(#[from] MissingKeyError),
190
191 #[error(transparent)]
192 #[diagnostic(transparent)]
193 MissingVariable(#[from] MissingVariableWarning),
194
195 #[error(transparent)]
196 #[diagnostic(transparent)]
197 SyntaxError(#[from] FtlSyntaxError),
198}
199
200#[derive(Debug, Diagnostic, Error)]
202#[error("failed to format {path}")]
203#[diagnostic(code(es_fluent::format::failed))]
204pub struct FormatError {
205 pub path: PathBuf,
207
208 #[help]
210 pub help: String,
211}
212
213#[derive(Debug, Diagnostic, Error)]
215#[error("formatted {formatted_count} file(s), {error_count} error(s)")]
216#[diagnostic(code(es_fluent::format::report))]
217pub struct FormatReport {
218 pub formatted_count: usize,
220
221 pub error_count: usize,
223
224 #[related]
226 pub errors: Vec<FormatError>,
227}
228
229#[derive(Debug, Diagnostic, Error)]
231#[error("missing translation for key '{key}' in locale '{target_locale}'")]
232#[diagnostic(code(es_fluent::sync::missing), severity(Warning))]
233pub struct SyncMissingKey {
234 pub key: String,
236
237 pub target_locale: String,
239
240 pub source_locale: String,
242}
243
244#[derive(Debug, Diagnostic, Error)]
246#[error("sync: added {added_count} key(s) to {locale_count} locale(s)")]
247#[diagnostic(code(es_fluent::sync::report))]
248pub struct SyncReport {
249 pub added_count: usize,
251
252 pub locale_count: usize,
254
255 #[related]
257 pub synced_keys: Vec<SyncMissingKey>,
258}
259
260#[derive(Debug, Diagnostic, Error)]
261pub enum CliError {
262 #[error(transparent)]
263 #[diagnostic(transparent)]
264 ConfigNotFound(#[from] ConfigNotFoundError),
265
266 #[error(transparent)]
267 #[diagnostic(transparent)]
268 ConfigParse(#[from] ConfigParseError),
269
270 #[error(transparent)]
271 #[diagnostic(transparent)]
272 AssetsNotFound(#[from] AssetsNotFoundError),
273
274 #[error(transparent)]
275 #[diagnostic(transparent)]
276 FallbackNotFound(#[from] FallbackLanguageNotFoundError),
277
278 #[error(transparent)]
279 #[diagnostic(transparent)]
280 InvalidLanguage(#[from] InvalidLanguageError),
281
282 #[error(transparent)]
283 #[diagnostic(transparent)]
284 LocaleNotFound(#[from] LocaleNotFoundError),
285
286 #[error(transparent)]
287 #[diagnostic(transparent)]
288 Validation(#[from] ValidationReport),
289
290 #[error(transparent)]
291 #[diagnostic(transparent)]
292 Format(#[from] FormatReport),
293
294 #[error(transparent)]
295 #[diagnostic(transparent)]
296 Sync(#[from] SyncReport),
297
298 #[error("IO error: {0}")]
299 #[diagnostic(code(es_fluent::io))]
300 Io(#[from] std::io::Error),
301
302 #[error("{0}")]
303 #[diagnostic(code(es_fluent::other))]
304 Other(String),
305}
306
307impl From<anyhow::Error> for CliError {
308 fn from(err: anyhow::Error) -> Self {
309 CliError::Other(err.to_string())
310 }
311}
312
313#[allow(dead_code)]
315pub fn span_from_line_col(source: &str, line: usize, col: usize, len: usize) -> SourceSpan {
316 let mut offset = 0;
317 for (i, line_content) in source.lines().enumerate() {
318 if i + 1 == line {
319 offset += col.saturating_sub(1);
320 break;
321 }
322 offset += line_content.len() + 1; }
324 SourceSpan::new(offset.into(), len)
325}
326
327#[allow(dead_code)]
329pub fn find_key_span(source: &str, key: &str) -> Option<SourceSpan> {
330 for (line_idx, line) in source.lines().enumerate() {
332 let trimmed = line.trim_start();
333 if let Some(rest) = trimmed.strip_prefix(key)
334 && (rest.starts_with(" =") || rest.starts_with('='))
335 {
336 let line_start: usize = source.lines().take(line_idx).map(|l| l.len() + 1).sum();
338 let key_start = line_start + (line.len() - trimmed.len());
339 return Some(SourceSpan::new(key_start.into(), key.len()));
340 }
341 }
342 None
343}
344
345#[allow(dead_code)]
347pub fn find_message_span(source: &str, key: &str) -> Option<SourceSpan> {
348 let mut in_message = false;
349 let mut start_offset = 0;
350 let mut current_offset = 0;
351
352 for line in source.lines() {
353 let trimmed = line.trim_start();
354
355 if let Some(rest) = trimmed.strip_prefix(key) {
356 if rest.starts_with(" =") || rest.starts_with('=') {
357 in_message = true;
358 start_offset = current_offset + (line.len() - trimmed.len());
359 }
360 } else if in_message {
361 if !line.starts_with(' ') && !line.starts_with('\t') && !trimmed.is_empty() {
363 let end_offset = current_offset;
365 return Some(SourceSpan::new(
366 start_offset.into(),
367 end_offset - start_offset,
368 ));
369 }
370 }
371
372 current_offset += line.len() + 1; }
374
375 if in_message {
377 return Some(SourceSpan::new(
378 start_offset.into(),
379 current_offset.saturating_sub(1) - start_offset,
380 ));
381 }
382
383 None
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 #[test]
391 fn test_find_key_span() {
392 let source = "## Comment\nhello = Hello\nworld = World";
393 let span = find_key_span(source, "hello").unwrap();
394 assert_eq!(span.offset(), 11);
395 assert_eq!(span.len(), 5);
396 }
397
398 #[test]
399 fn test_find_key_span_with_spaces() {
400 let source = "hello =Hello\nworld = World";
401 let span = find_key_span(source, "hello").unwrap();
402 assert_eq!(span.offset(), 0);
403 assert_eq!(span.len(), 5);
404 }
405
406 #[test]
407 fn test_find_message_span_multiline() {
408 let source = "greeting = Hello\n World\nnext = Next";
409 let span = find_message_span(source, "greeting").unwrap();
410 assert_eq!(span.offset(), 0);
411 }
413
414 #[test]
415 fn test_span_from_line_col() {
416 let source = "line1\nline2\nline3";
417 let span = span_from_line_col(source, 2, 1, 5);
418 assert_eq!(span.offset(), 6); assert_eq!(span.len(), 5);
420 }
421}