1#![doc = include_str!("../README.md")]
2
3use clap::ValueEnum;
4use es_fluent_core::meta::TypeKind;
5use es_fluent_core::namer::FluentKey;
6use es_fluent_core::registry::{FtlTypeInfo, FtlVariant};
7use fluent_syntax::{ast, parser};
8use std::collections::HashMap;
9use std::{fs, path::Path};
10
11pub mod clean;
12pub mod error;
13pub mod formatting;
14pub mod value;
15
16use error::FluentGenerateError;
17use value::ValueFormatter;
18
19#[derive(Clone, Debug, Default, PartialEq, ValueEnum)]
21pub enum FluentParseMode {
22 Aggressive,
24 #[default]
26 Conservative,
27}
28
29impl std::fmt::Display for FluentParseMode {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 match self {
32 Self::Aggressive => write!(f, "aggressive"),
33 Self::Conservative => write!(f, "conservative"),
34 }
35 }
36}
37
38pub fn generate<P: AsRef<Path>>(
40 crate_name: &str,
41 i18n_path: P,
42 items: Vec<FtlTypeInfo>,
43 mode: FluentParseMode,
44 dry_run: bool,
45) -> Result<bool, FluentGenerateError> {
46 let i18n_path = i18n_path.as_ref();
47
48 if !dry_run {
49 fs::create_dir_all(i18n_path)?;
50 }
51
52 let file_path = i18n_path.join(format!("{}.ftl", crate_name));
53
54 let existing_resource = read_existing_resource(&file_path)?;
55
56 let final_resource = if matches!(mode, FluentParseMode::Aggressive) {
57 build_target_resource(&items)
59 } else {
60 smart_merge(existing_resource, &items, MergeBehavior::Append)
62 };
63
64 write_updated_resource(
65 &file_path,
66 &final_resource,
67 dry_run,
68 formatting::sort_ftl_resource,
69 )
70}
71
72pub(crate) fn print_diff(old: &str, new: &str) {
73 use colored::Colorize as _;
74 use similar::{ChangeTag, TextDiff};
75
76 let diff = TextDiff::from_lines(old, new);
77
78 for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
79 if idx > 0 {
80 println!("{}", " ...".dimmed());
81 }
82 for op in group {
83 for change in diff.iter_changes(op) {
84 let sign = match change.tag() {
85 ChangeTag::Delete => "-",
86 ChangeTag::Insert => "+",
87 ChangeTag::Equal => " ",
88 };
89 let line = format!("{} {}", sign, change);
90 match change.tag() {
91 ChangeTag::Delete => print!("{}", line.red()),
92 ChangeTag::Insert => print!("{}", line.green()),
93 ChangeTag::Equal => print!("{}", line.dimmed()),
94 }
95 }
96 }
97 }
98}
99
100fn read_existing_resource(file_path: &Path) -> Result<ast::Resource<String>, FluentGenerateError> {
105 if !file_path.exists() {
106 return Ok(ast::Resource { body: Vec::new() });
107 }
108
109 let content = fs::read_to_string(file_path)?;
110 if content.trim().is_empty() {
111 return Ok(ast::Resource { body: Vec::new() });
112 }
113
114 match parser::parse(content) {
115 Ok(res) => Ok(res),
116 Err((res, errors)) => {
117 tracing::warn!(
118 "Warning: Encountered parsing errors in {}: {:?}",
119 file_path.display(),
120 errors
121 );
122 Ok(res)
123 },
124 }
125}
126
127fn write_updated_resource(
131 file_path: &Path,
132 resource: &ast::Resource<String>,
133 dry_run: bool,
134 formatter: impl Fn(&ast::Resource<String>) -> String,
135) -> Result<bool, FluentGenerateError> {
136 let is_empty = resource.body.is_empty();
137 let final_content = if is_empty {
138 String::new()
139 } else {
140 formatter(resource)
141 };
142
143 let current_content = if file_path.exists() {
144 fs::read_to_string(file_path)?
145 } else {
146 String::new()
147 };
148
149 let has_changed = match is_empty {
151 true => current_content != final_content && !current_content.trim().is_empty(),
152 false => current_content.trim() != final_content.trim(),
153 };
154
155 if !has_changed {
156 log_unchanged(file_path, is_empty, dry_run);
157 return Ok(false);
158 }
159
160 write_or_preview(
161 file_path,
162 ¤t_content,
163 &final_content,
164 is_empty,
165 dry_run,
166 )?;
167 Ok(true)
168}
169
170fn log_unchanged(file_path: &Path, is_empty: bool, dry_run: bool) {
172 if dry_run {
173 return;
174 }
175 let msg = match is_empty {
176 true => format!(
177 "FTL file unchanged (empty or no items): {}",
178 file_path.display()
179 ),
180 false => format!("FTL file unchanged: {}", file_path.display()),
181 };
182 tracing::debug!("{}", msg);
183}
184
185fn write_or_preview(
187 file_path: &Path,
188 current_content: &str,
189 final_content: &str,
190 is_empty: bool,
191 dry_run: bool,
192) -> Result<(), FluentGenerateError> {
193 if dry_run {
194 let display_path = fs::canonicalize(file_path).unwrap_or_else(|_| file_path.to_path_buf());
195 let msg = match (is_empty, !current_content.trim().is_empty()) {
196 (true, true) => format!(
197 "Would write empty FTL file (no items): {}",
198 display_path.display()
199 ),
200 (true, false) => format!("Would write empty FTL file: {}", display_path.display()),
201 (false, _) => format!("Would update FTL file: {}", display_path.display()),
202 };
203 println!("{}", msg);
204 print_diff(current_content, final_content);
205 println!();
206 return Ok(());
207 }
208
209 fs::write(file_path, final_content)?;
210 let msg = match is_empty {
211 true => format!("Wrote empty FTL file (no items): {}", file_path.display()),
212 false => format!("Updated FTL file: {}", file_path.display()),
213 };
214 tracing::info!("{}", msg);
215 Ok(())
216}
217
218fn compare_type_infos(a: &FtlTypeInfo, b: &FtlTypeInfo) -> std::cmp::Ordering {
220 let a_is_this = a
222 .variants
223 .iter()
224 .any(|v| v.ftl_key.to_string().ends_with(FluentKey::THIS_SUFFIX));
225 let b_is_this = b
226 .variants
227 .iter()
228 .any(|v| v.ftl_key.to_string().ends_with(FluentKey::THIS_SUFFIX));
229
230 formatting::compare_with_this_priority(a_is_this, &a.type_name, b_is_this, &b.type_name)
231}
232
233#[derive(Clone, Copy, Debug, PartialEq)]
234pub(crate) enum MergeBehavior {
235 Append,
237 Clean,
239}
240
241pub(crate) fn smart_merge(
242 existing: ast::Resource<String>,
243 items: &[FtlTypeInfo],
244 behavior: MergeBehavior,
245) -> ast::Resource<String> {
246 let mut pending_items = merge_ftl_type_infos(items);
247 pending_items.sort_by(compare_type_infos);
248
249 let mut item_map: HashMap<String, FtlTypeInfo> = pending_items
250 .into_iter()
251 .map(|i| (i.type_name.clone(), i))
252 .collect();
253
254 let mut new_body = Vec::new();
255 let mut current_group_name: Option<String> = None;
256 let cleanup = matches!(behavior, MergeBehavior::Clean);
257
258 for entry in existing.body {
259 match entry {
260 ast::Entry::GroupComment(ref comment) => {
261 if let Some(ref old_group) = current_group_name
262 && let Some(info) = item_map.get_mut(old_group)
263 && !info.variants.is_empty()
264 {
265 if matches!(behavior, MergeBehavior::Append) {
267 for variant in &info.variants {
268 new_body.push(create_message_entry(variant));
269 }
270 }
271 info.variants.clear();
272 }
273
274 if let Some(content) = comment.content.first() {
275 let trimmed = content.trim();
276 current_group_name = Some(trimmed.to_string());
277 } else {
278 current_group_name = None;
279 }
280
281 let keep_group = if let Some(ref group_name) = current_group_name {
282 !cleanup || item_map.contains_key(group_name)
283 } else {
284 true
285 };
286
287 if keep_group {
288 new_body.push(entry);
289 }
290 },
291 ast::Entry::Message(ref msg) => {
292 let key = &msg.id.name;
293 let mut handled = false;
294
295 if let Some(ref group_name) = current_group_name
296 && let Some(info) = item_map.get_mut(group_name)
297 && let Some(idx) = info
298 .variants
299 .iter()
300 .position(|v| v.ftl_key.to_string() == *key)
301 {
302 info.variants.remove(idx);
303 handled = true;
304 }
305
306 if !handled {
307 for info in item_map.values_mut() {
308 if let Some(idx) = info
309 .variants
310 .iter()
311 .position(|v| v.ftl_key.to_string() == *key)
312 {
313 info.variants.remove(idx);
314 handled = true;
315 break;
316 }
317 }
318 }
319
320 if handled || !cleanup {
321 new_body.push(entry);
322 }
323 },
324 ast::Entry::Term(ref term) => {
325 let key = format!("{}{}", FluentKey::DELIMITER, term.id.name);
326 let mut handled = false;
327 for info in item_map.values_mut() {
328 if let Some(idx) = info
329 .variants
330 .iter()
331 .position(|v| v.ftl_key.to_string() == key)
332 {
333 info.variants.remove(idx);
334 handled = true;
335 break;
336 }
337 }
338
339 if handled || !cleanup {
340 new_body.push(entry);
341 }
342 },
343 ast::Entry::Junk { .. } => {
344 new_body.push(entry);
345 },
346 _ => {
347 new_body.push(entry);
348 },
349 }
350 }
351
352 if let Some(ref last_group) = current_group_name
354 && let Some(info) = item_map.get_mut(last_group)
355 && !info.variants.is_empty()
356 {
357 if matches!(behavior, MergeBehavior::Append) {
359 for variant in &info.variants {
360 new_body.push(create_message_entry(variant));
361 }
362 }
363 info.variants.clear();
364 }
365
366 if matches!(behavior, MergeBehavior::Append) {
368 let mut remaining_groups: Vec<_> = item_map.into_iter().collect();
369 remaining_groups.sort_by(|(_, a), (_, b)| compare_type_infos(a, b));
370
371 for (type_name, info) in remaining_groups {
372 if !info.variants.is_empty() {
373 new_body.push(create_group_comment_entry(&type_name));
374 for variant in info.variants {
375 new_body.push(create_message_entry(&variant));
376 }
377 }
378 }
379 }
380
381 ast::Resource { body: new_body }
382}
383
384fn create_group_comment_entry(type_name: &str) -> ast::Entry<String> {
385 ast::Entry::GroupComment(ast::Comment {
386 content: vec![type_name.to_owned()],
387 })
388}
389
390fn create_message_entry(variant: &FtlVariant) -> ast::Entry<String> {
391 let message_id = ast::Identifier {
392 name: variant.ftl_key.to_string(),
393 };
394
395 let base_value = ValueFormatter::expand(&variant.name);
396
397 let mut elements = vec![ast::PatternElement::TextElement { value: base_value }];
398
399 for arg_name in &variant.args {
400 elements.push(ast::PatternElement::TextElement { value: " ".into() });
401
402 elements.push(ast::PatternElement::Placeable {
403 expression: ast::Expression::Inline(ast::InlineExpression::VariableReference {
404 id: ast::Identifier {
405 name: arg_name.into(),
406 },
407 }),
408 });
409 }
410
411 let pattern = ast::Pattern { elements };
412
413 ast::Entry::Message(ast::Message {
414 id: message_id,
415 value: Some(pattern),
416 attributes: Vec::new(),
417 comment: None,
418 })
419}
420
421fn merge_ftl_type_infos(items: &[FtlTypeInfo]) -> Vec<FtlTypeInfo> {
422 use std::collections::BTreeMap;
423
424 let mut grouped: BTreeMap<String, (TypeKind, Vec<FtlVariant>, String)> = BTreeMap::new();
426
427 for item in items {
428 let entry = grouped
429 .entry(item.type_name.clone())
430 .or_insert_with(|| (item.type_kind.clone(), Vec::new(), item.module_path.clone()));
431 entry.1.extend(item.variants.clone());
432 }
433
434 grouped
435 .into_iter()
436 .map(|(type_name, (type_kind, mut variants, module_path))| {
437 variants.sort_by(|a, b| {
438 let a_is_this = a.ftl_key.to_string().ends_with(FluentKey::THIS_SUFFIX);
439 let b_is_this = b.ftl_key.to_string().ends_with(FluentKey::THIS_SUFFIX);
440 formatting::compare_with_this_priority(a_is_this, &a.name, b_is_this, &b.name)
441 });
442 variants.dedup();
443
444 FtlTypeInfo {
445 type_kind,
446 type_name,
447 variants,
448 file_path: None,
449 module_path,
450 }
451 })
452 .collect()
453}
454
455fn build_target_resource(items: &[FtlTypeInfo]) -> ast::Resource<String> {
456 let items = merge_ftl_type_infos(items);
457 let mut body: Vec<ast::Entry<String>> = Vec::new();
458 let mut sorted_items = items.to_vec();
459 sorted_items.sort_by(compare_type_infos);
460
461 for info in &sorted_items {
462 body.push(create_group_comment_entry(&info.type_name));
463
464 for variant in &info.variants {
465 body.push(create_message_entry(variant));
466 }
467 }
468
469 ast::Resource { body }
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475 use es_fluent_core::{meta::TypeKind, namer::FluentKey};
476 use proc_macro2::Ident;
477 use std::fs;
478 use tempfile::TempDir;
479
480 #[test]
481 fn test_value_formatter_expand() {
482 assert_eq!(ValueFormatter::expand("simple-key"), "Key");
483 assert_eq!(ValueFormatter::expand("another-test-value"), "Value");
484 assert_eq!(ValueFormatter::expand("single"), "Single");
485 }
486
487 #[test]
488 fn test_generate_empty_items() {
489 let temp_dir = TempDir::new().unwrap();
490 let i18n_path = temp_dir.path().join("i18n");
491
492 let result = generate(
493 "test_crate",
494 &i18n_path,
495 vec![],
496 FluentParseMode::Conservative,
497 false,
498 );
499 assert!(result.is_ok());
500
501 let ftl_file_path = i18n_path.join("test_crate.ftl");
502 assert!(!ftl_file_path.exists());
503 }
504
505 #[test]
506 fn test_generate_with_items() {
507 let temp_dir = TempDir::new().unwrap();
508 let i18n_path = temp_dir.path().join("i18n");
509
510 let ftl_key = FluentKey::from(&Ident::new("TestEnum", proc_macro2::Span::call_site()))
511 .join("Variant1");
512 let variant = FtlVariant {
513 name: "variant1".to_string(),
514 ftl_key,
515 args: Vec::new(),
516 module_path: "test".to_string(),
517 };
518
519 let type_info = FtlTypeInfo {
520 type_kind: TypeKind::Enum,
521 type_name: "TestEnum".to_string(),
522 variants: vec![variant],
523 file_path: None,
524 module_path: "test".to_string(),
525 };
526
527 let result = generate(
528 "test_crate",
529 &i18n_path,
530 vec![type_info],
531 FluentParseMode::Conservative,
532 false,
533 );
534 assert!(result.is_ok());
535
536 let ftl_file_path = i18n_path.join("test_crate.ftl");
537 assert!(ftl_file_path.exists());
538
539 let content = fs::read_to_string(ftl_file_path).unwrap();
540 assert!(content.contains("TestEnum"));
541 assert!(content.contains("Variant1"));
542 }
543
544 #[test]
545 fn test_generate_aggressive_mode() {
546 let temp_dir = TempDir::new().unwrap();
547 let i18n_path = temp_dir.path().join("i18n");
548
549 let ftl_file_path = i18n_path.join("test_crate.ftl");
550 fs::create_dir_all(&i18n_path).unwrap();
551 fs::write(&ftl_file_path, "existing-message = Existing Content").unwrap();
552
553 let ftl_key = FluentKey::from(&Ident::new("TestEnum", proc_macro2::Span::call_site()))
554 .join("Variant1");
555 let variant = FtlVariant {
556 name: "variant1".to_string(),
557 ftl_key,
558 args: Vec::new(),
559 module_path: "test".to_string(),
560 };
561
562 let type_info = FtlTypeInfo {
563 type_kind: TypeKind::Enum,
564 type_name: "TestEnum".to_string(),
565 variants: vec![variant],
566 file_path: None,
567 module_path: "test".to_string(),
568 };
569
570 let result = generate(
571 "test_crate",
572 &i18n_path,
573 vec![type_info],
574 FluentParseMode::Aggressive,
575 false,
576 );
577 assert!(result.is_ok());
578
579 let content = fs::read_to_string(&ftl_file_path).unwrap();
580 assert!(!content.contains("existing-message"));
581 assert!(content.contains("TestEnum"));
582 assert!(content.contains("Variant1"));
583 }
584
585 #[test]
586 fn test_generate_conservative_mode() {
587 let temp_dir = TempDir::new().unwrap();
588 let i18n_path = temp_dir.path().join("i18n");
589
590 let ftl_file_path = i18n_path.join("test_crate.ftl");
591 fs::create_dir_all(&i18n_path).unwrap();
592 fs::write(&ftl_file_path, "existing-message = Existing Content").unwrap();
593
594 let ftl_key = FluentKey::from(&Ident::new("TestEnum", proc_macro2::Span::call_site()))
595 .join("Variant1");
596 let variant = FtlVariant {
597 name: "variant1".to_string(),
598 ftl_key,
599 args: Vec::new(),
600 module_path: "test".to_string(),
601 };
602
603 let type_info = FtlTypeInfo {
604 type_kind: TypeKind::Enum,
605 type_name: "TestEnum".to_string(),
606 variants: vec![variant],
607 file_path: None,
608 module_path: "test".to_string(),
609 };
610
611 let result = generate(
612 "test_crate",
613 &i18n_path,
614 vec![type_info],
615 FluentParseMode::Conservative,
616 false,
617 );
618 assert!(result.is_ok());
619
620 let content = fs::read_to_string(&ftl_file_path).unwrap();
621 assert!(content.contains("existing-message"));
622 assert!(content.contains("TestEnum"));
623 assert!(content.contains("Variant1"));
624 }
625 #[test]
626 fn test_generate_clean_mode() {
627 let temp_dir = TempDir::new().unwrap();
628 let i18n_path = temp_dir.path().join("i18n");
629
630 let ftl_file_path = i18n_path.join("test_crate.ftl");
631 fs::create_dir_all(&i18n_path).unwrap();
632
633 let initial_content = "
634## OrphanGroup
635
636what-Hi = Hi
637awdawd = awdwa
638
639## ExistingGroup
640
641existing-key = Existing Value
642";
643 fs::write(&ftl_file_path, initial_content).unwrap();
644
645 let ftl_key = FluentKey::from(&Ident::new("ExistingGroup", proc_macro2::Span::call_site()))
647 .join("ExistingKey");
648 let variant = FtlVariant {
649 name: "ExistingKey".to_string(),
650 ftl_key,
651 args: Vec::new(),
652 module_path: "test".to_string(),
653 };
654
655 let type_info = FtlTypeInfo {
656 type_kind: TypeKind::Enum,
657 type_name: "ExistingGroup".to_string(),
658 variants: vec![variant],
659 file_path: None,
660 module_path: "test".to_string(),
661 };
662
663 let result = crate::clean::clean("test_crate", &i18n_path, vec![type_info], false);
664 assert!(result.is_ok());
665
666 let content = fs::read_to_string(&ftl_file_path).unwrap();
667
668 assert!(!content.contains("## OrphanGroup"));
670 assert!(!content.contains("what-Hi"));
671 assert!(!content.contains("awdawd"));
672
673 assert!(content.contains("## ExistingGroup"));
675 }
676
677 #[test]
678 fn test_this_types_sorted_first() {
679 let temp_dir = TempDir::new().unwrap();
680 let i18n_path = temp_dir.path().join("i18n");
681
682 let apple_variant = FtlVariant {
684 name: "Red".to_string(),
685 ftl_key: FluentKey::from(&Ident::new("Apple", proc_macro2::Span::call_site()))
686 .join("Red"),
687 args: Vec::new(),
688 module_path: "test".to_string(),
689 };
690 let apple = FtlTypeInfo {
691 type_kind: TypeKind::Enum,
692 type_name: "Apple".to_string(),
693 variants: vec![apple_variant],
694 file_path: None,
695 module_path: "test".to_string(),
696 };
697
698 let banana_variant = FtlVariant {
699 name: "Yellow".to_string(),
700 ftl_key: FluentKey::from(&Ident::new("Banana", proc_macro2::Span::call_site()))
701 .join("Yellow"),
702 args: Vec::new(),
703 module_path: "test".to_string(),
704 };
705 let banana = FtlTypeInfo {
706 type_kind: TypeKind::Enum,
707 type_name: "Banana".to_string(),
708 variants: vec![banana_variant],
709 file_path: None,
710 module_path: "test".to_string(),
711 };
712
713 let banana_this_ident = Ident::new("BananaThis", proc_macro2::Span::call_site());
716 let banana_this_key = FluentKey::new_this(&banana_this_ident); let banana_this_variant = FtlVariant {
719 name: "this".to_string(),
720 ftl_key: banana_this_key,
721 args: Vec::new(),
722 module_path: "test".to_string(),
723 };
724 let banana_this = FtlTypeInfo {
725 type_kind: TypeKind::Struct,
726 type_name: "BananaThis".to_string(),
727 variants: vec![banana_this_variant],
728 file_path: None,
729 module_path: "test".to_string(),
730 };
731
732 let result = generate(
733 "test_crate",
734 &i18n_path,
735 vec![apple.clone(), banana.clone(), banana_this.clone()],
736 FluentParseMode::Aggressive,
737 false,
738 );
739 assert!(result.is_ok());
740
741 let ftl_file_path = i18n_path.join("test_crate.ftl");
742 let content = fs::read_to_string(&ftl_file_path).unwrap();
743
744 let banana_this_pos = content.find("## BananaThis").expect("BananaThis missing");
746 let apple_pos = content.find("## Apple").expect("Apple missing");
747 let banana_pos = content.find("## Banana\n").expect("Banana missing");
748
749 assert!(
750 banana_this_pos < apple_pos,
751 "BananaThis (is_this=true) should come before Apple"
752 );
753 assert!(
754 banana_this_pos < banana_pos,
755 "BananaThis (is_this=true) should come before Banana"
756 );
757 assert!(apple_pos < banana_pos, "Apple should come before Banana");
759 }
760
761 #[test]
762 fn test_this_variants_sorted_first_within_group() {
763 let temp_dir = TempDir::new().unwrap();
764 let i18n_path = temp_dir.path().join("i18n");
765
766 let fruit_ident = Ident::new("Fruit", proc_macro2::Span::call_site());
769 let this_key = FluentKey::new_this(&fruit_ident); let this_variant = FtlVariant {
772 name: "this".to_string(),
773 ftl_key: this_key,
774 args: Vec::new(),
775 module_path: "test".to_string(),
776 };
777 let apple_variant = FtlVariant {
778 name: "Apple".to_string(),
779 ftl_key: FluentKey::from(&fruit_ident).join("Apple"),
780 args: Vec::new(),
781 module_path: "test".to_string(),
782 };
783 let banana_variant = FtlVariant {
784 name: "Banana".to_string(),
785 ftl_key: FluentKey::from(&fruit_ident).join("Banana"),
786 args: Vec::new(),
787 module_path: "test".to_string(),
788 };
789
790 let fruit = FtlTypeInfo {
791 type_kind: TypeKind::Enum,
792 type_name: "Fruit".to_string(),
793 variants: vec![
795 banana_variant.clone(),
796 this_variant.clone(),
797 apple_variant.clone(),
798 ],
799 file_path: None,
800 module_path: "test".to_string(),
801 };
802
803 let result = generate(
804 "test_crate",
805 &i18n_path,
806 vec![fruit],
807 FluentParseMode::Aggressive,
808 false,
809 );
810 assert!(result.is_ok());
811
812 let ftl_file_path = i18n_path.join("test_crate.ftl");
813 let content = fs::read_to_string(&ftl_file_path).unwrap();
814
815 let this_pos = content
817 .find("fruit_this =")
818 .expect("this variant (fruit_this) missing");
819 let apple_pos = content.find("fruit-Apple").expect("Apple variant missing");
820 let banana_pos = content
821 .find("fruit-Banana")
822 .expect("Banana variant missing");
823
824 assert!(
825 this_pos < apple_pos,
826 "This variant should come before Apple"
827 );
828 assert!(
829 this_pos < banana_pos,
830 "This variant should come before Banana"
831 );
832 assert!(apple_pos < banana_pos, "Apple should come before Banana");
833 }
834}