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")]
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 #[help]
162 pub help: String,
163}
164
165#[derive(Debug, Diagnostic, Error)]
167#[error("validation found {error_count} error(s) and {warning_count} warning(s)")]
168#[diagnostic(code(es_fluent::validate::report))]
169pub struct ValidationReport {
170 pub error_count: usize,
172
173 pub warning_count: usize,
175
176 #[related]
178 pub issues: Vec<ValidationIssue>,
179}
180
181#[derive(Debug, Diagnostic, Error)]
183pub enum ValidationIssue {
184 #[error(transparent)]
185 #[diagnostic(transparent)]
186 MissingKey(#[from] MissingKeyError),
187
188 #[error(transparent)]
189 #[diagnostic(transparent)]
190 MissingVariable(#[from] MissingVariableWarning),
191
192 #[error(transparent)]
193 #[diagnostic(transparent)]
194 SyntaxError(#[from] FtlSyntaxError),
195}
196
197impl ValidationIssue {
198 pub fn sort_key(&self) -> String {
205 match self {
206 ValidationIssue::SyntaxError(e) => {
207 format!("1:{:?}", e.src.name())
208 },
209 ValidationIssue::MissingKey(e) => {
210 format!("2:{:?}:{}", e.src.name(), e.key)
211 },
212 ValidationIssue::MissingVariable(e) => {
213 format!("3:{:?}:{}:{}", e.src.name(), e.key, e.variable)
214 },
215 }
216 }
217}
218
219#[derive(Debug, Diagnostic, Error)]
221#[error("failed to format {path}")]
222#[diagnostic(code(es_fluent::format::failed))]
223pub struct FormatError {
224 pub path: PathBuf,
226
227 #[help]
229 pub help: String,
230}
231
232#[derive(Debug, Diagnostic, Error)]
234#[error("formatted {formatted_count} file(s), {error_count} error(s)")]
235#[diagnostic(code(es_fluent::format::report))]
236pub struct FormatReport {
237 pub formatted_count: usize,
239
240 pub error_count: usize,
242
243 #[related]
245 pub errors: Vec<FormatError>,
246}
247
248#[derive(Debug, Diagnostic, Error)]
250#[error("missing translation for key '{key}' in locale '{target_locale}'")]
251#[diagnostic(code(es_fluent::sync::missing), severity(Warning))]
252pub struct SyncMissingKey {
253 pub key: String,
255
256 pub target_locale: String,
258
259 pub source_locale: String,
261}
262
263#[derive(Debug, Diagnostic, Error)]
265#[error("sync: added {added_count} key(s) to {locale_count} locale(s)")]
266#[diagnostic(code(es_fluent::sync::report))]
267pub struct SyncReport {
268 pub added_count: usize,
270
271 pub locale_count: usize,
273
274 #[related]
276 pub synced_keys: Vec<SyncMissingKey>,
277}
278
279#[derive(Debug, Diagnostic, Error)]
280pub enum CliError {
281 #[error(transparent)]
282 #[diagnostic(transparent)]
283 ConfigNotFound(#[from] ConfigNotFoundError),
284
285 #[error(transparent)]
286 #[diagnostic(transparent)]
287 ConfigParse(#[from] ConfigParseError),
288
289 #[error(transparent)]
290 #[diagnostic(transparent)]
291 AssetsNotFound(#[from] AssetsNotFoundError),
292
293 #[error(transparent)]
294 #[diagnostic(transparent)]
295 FallbackNotFound(#[from] FallbackLanguageNotFoundError),
296
297 #[error(transparent)]
298 #[diagnostic(transparent)]
299 InvalidLanguage(#[from] InvalidLanguageError),
300
301 #[error(transparent)]
302 #[diagnostic(transparent)]
303 LocaleNotFound(#[from] LocaleNotFoundError),
304
305 #[error(transparent)]
306 #[diagnostic(transparent)]
307 Validation(#[from] ValidationReport),
308
309 #[error(transparent)]
310 #[diagnostic(transparent)]
311 Format(#[from] FormatReport),
312
313 #[error(transparent)]
314 #[diagnostic(transparent)]
315 Sync(#[from] SyncReport),
316
317 #[error("IO error: {0}")]
318 #[diagnostic(code(es_fluent::io))]
319 Io(#[from] std::io::Error),
320
321 #[error("{0}")]
322 #[diagnostic(code(es_fluent::other))]
323 Other(String),
324}
325
326impl From<anyhow::Error> for CliError {
327 fn from(err: anyhow::Error) -> Self {
328 CliError::Other(err.to_string())
329 }
330}
331
332pub fn line_col_from_offset(source: &str, offset: usize) -> (usize, usize) {
334 let mut current_offset = 0;
335 for (i, line) in source.lines().enumerate() {
336 let line_len = line.len() + 1; if current_offset + line_len > offset {
338 let col = offset - current_offset + 1;
339 return (i + 1, col);
340 }
341 current_offset += line_len;
342 }
343 (source.lines().count().max(1), 1)
344}
345
346#[allow(dead_code)]
348pub fn span_from_line_col(source: &str, line: usize, col: usize, len: usize) -> SourceSpan {
349 let mut offset = 0;
350 for (i, line_content) in source.lines().enumerate() {
351 if i + 1 == line {
352 offset += col.saturating_sub(1);
353 break;
354 }
355 offset += line_content.len() + 1; }
357 SourceSpan::new(offset.into(), len)
358}
359
360#[allow(dead_code)]
362pub fn find_key_span(source: &str, key: &str) -> Option<SourceSpan> {
363 for (line_idx, line) in source.lines().enumerate() {
365 let trimmed = line.trim_start();
366 if let Some(rest) = trimmed.strip_prefix(key)
367 && (rest.starts_with(" =") || rest.starts_with('='))
368 {
369 let line_start: usize = source.lines().take(line_idx).map(|l| l.len() + 1).sum();
371 let key_start = line_start + (line.len() - trimmed.len());
372 return Some(SourceSpan::new(key_start.into(), key.len()));
373 }
374 }
375 None
376}
377
378#[allow(dead_code)]
380pub fn find_message_span(source: &str, key: &str) -> Option<SourceSpan> {
381 let mut in_message = false;
382 let mut start_offset = 0;
383 let mut current_offset = 0;
384
385 for line in source.lines() {
386 let trimmed = line.trim_start();
387
388 if let Some(rest) = trimmed.strip_prefix(key) {
389 if rest.starts_with(" =") || rest.starts_with('=') {
390 in_message = true;
391 start_offset = current_offset + (line.len() - trimmed.len());
392 }
393 } else if in_message {
394 if !line.starts_with(' ') && !line.starts_with('\t') && !trimmed.is_empty() {
396 let end_offset = current_offset;
398 return Some(SourceSpan::new(
399 start_offset.into(),
400 end_offset - start_offset,
401 ));
402 }
403 }
404
405 current_offset += line.len() + 1; }
407
408 if in_message {
410 return Some(SourceSpan::new(
411 start_offset.into(),
412 current_offset.saturating_sub(1) - start_offset,
413 ));
414 }
415
416 None
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422
423 #[test]
424 fn test_find_key_span() {
425 let source = "## Comment\nhello = Hello\nworld = World";
426 let span = find_key_span(source, "hello").unwrap();
427 assert_eq!(span.offset(), 11);
428 assert_eq!(span.len(), 5);
429 }
430
431 #[test]
432 fn test_find_key_span_with_spaces() {
433 let source = "hello =Hello\nworld = World";
434 let span = find_key_span(source, "hello").unwrap();
435 assert_eq!(span.offset(), 0);
436 assert_eq!(span.len(), 5);
437 }
438
439 #[test]
440 fn test_find_message_span_multiline() {
441 let source = "greeting = Hello\n World\nnext = Next";
442 let span = find_message_span(source, "greeting").unwrap();
443 assert_eq!(span.offset(), 0);
444 }
446
447 #[test]
448 fn test_span_from_line_col() {
449 let source = "line1\nline2\nline3";
450 let span = span_from_line_col(source, 2, 1, 5);
451 assert_eq!(span.offset(), 6); assert_eq!(span.len(), 5);
453 }
454}