1use crate::citation::LocatorType;
7use crate::template::ContributorRole;
8
9use super::Locale;
10use super::types::{GeneralTerm, GrammaticalGender, MaybeGendered, SimpleTerm, TermForm};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum ArchiveHierarchyField {
15 Collection,
17 Series,
19 Box,
21 Folder,
23 Item,
25}
26
27impl ArchiveHierarchyField {
28 fn message_id(self) -> &'static str {
30 match self {
31 Self::Collection => "term.archive-collection-label",
32 Self::Series => "term.archive-series-label",
33 Self::Box => "term.archive-box-label",
34 Self::Folder => "term.archive-folder-label",
35 Self::Item => "term.archive-item-label",
36 }
37 }
38}
39
40impl Locale {
41 fn resolve_gendered_value(
43 value: &MaybeGendered<String>,
44 requested_gender: Option<GrammaticalGender>,
45 ) -> Option<&str> {
46 value
47 .resolve_with_fallback(requested_gender)
48 .map(String::as_str)
49 }
50
51 fn resolve_gendered_value_neutral(value: &MaybeGendered<String>) -> Option<&str> {
52 value.resolve_neutral().map(String::as_str)
53 }
54
55 fn resolve_no_date_value<'a>(
56 value: &'a SimpleTerm,
57 form: &TermForm,
58 requested_gender: Option<GrammaticalGender>,
59 ) -> Option<&'a str> {
60 match requested_gender {
61 Some(GrammaticalGender::Common) => match *form {
62 TermForm::Long => value
63 .long
64 .resolve_strict(Some(GrammaticalGender::Common))
65 .map(String::as_str),
66 TermForm::Short => value
67 .short
68 .resolve_strict(Some(GrammaticalGender::Common))
69 .map(String::as_str)
70 .filter(|value| !value.is_empty())
71 .or_else(|| {
72 value
73 .long
74 .resolve_strict(Some(GrammaticalGender::Common))
75 .map(String::as_str)
76 }),
77 _ => value
78 .long
79 .resolve_strict(Some(GrammaticalGender::Common))
80 .map(String::as_str),
81 },
82 _ => match *form {
83 TermForm::Long => Self::resolve_gendered_value(&value.long, requested_gender),
84 TermForm::Short => {
85 Self::resolve_gendered_value(&value.short, requested_gender.clone())
86 .filter(|value| !value.is_empty())
87 .or_else(|| Self::resolve_gendered_value(&value.long, requested_gender))
88 }
89 _ => Self::resolve_gendered_value(&value.long, requested_gender),
90 },
91 }
92 }
93
94 pub fn role_term(
96 &self,
97 role: &ContributorRole,
98 plural: bool,
99 form: &TermForm,
100 requested_gender: Option<GrammaticalGender>,
101 ) -> Option<&str> {
102 let term = self.roles.get(role)?;
103 let simple = if plural { &term.plural } else { &term.singular };
104 let term_text = match *form {
105 TermForm::Long => Self::resolve_gendered_value(&simple.long, requested_gender),
106 TermForm::Short => {
107 Self::resolve_gendered_value(&simple.short, requested_gender.clone())
108 .filter(|value| !value.is_empty())
109 .or_else(|| Self::resolve_gendered_value(&simple.long, requested_gender))
110 }
111 TermForm::Verb => Self::resolve_gendered_value(&term.verb.long, None),
112 TermForm::VerbShort => Self::resolve_gendered_value(&term.verb.short, None)
113 .filter(|value| !value.is_empty())
114 .or_else(|| Self::resolve_gendered_value(&term.verb.long, None)),
115 _ => Self::resolve_gendered_value(&simple.long, requested_gender),
116 };
117
118 match term_text {
119 Some(value) if !value.is_empty() => Some(value),
120 _ => None,
121 }
122 }
123
124 pub fn role_term_neutral(
126 &self,
127 role: &ContributorRole,
128 plural: bool,
129 form: &TermForm,
130 ) -> Option<&str> {
131 let term = self.roles.get(role)?;
132 let simple = if plural { &term.plural } else { &term.singular };
133 let term_text = match *form {
134 TermForm::Long => Self::resolve_gendered_value_neutral(&simple.long),
135 TermForm::Short => Self::resolve_gendered_value_neutral(&simple.short)
136 .filter(|value| !value.is_empty())
137 .or_else(|| Self::resolve_gendered_value_neutral(&simple.long)),
138 TermForm::Verb => Self::resolve_gendered_value(&term.verb.long, None),
139 TermForm::VerbShort => Self::resolve_gendered_value(&term.verb.short, None)
140 .filter(|value| !value.is_empty())
141 .or_else(|| Self::resolve_gendered_value(&term.verb.long, None)),
142 _ => Self::resolve_gendered_value_neutral(&simple.long),
143 };
144
145 match term_text {
146 Some(value) if !value.is_empty() => Some(value),
147 _ => None,
148 }
149 }
150
151 pub fn resolved_role_term(
153 &self,
154 role: &ContributorRole,
155 plural: bool,
156 form: &TermForm,
157 requested_gender: Option<GrammaticalGender>,
158 ) -> Option<String> {
159 if let Some(message_id) = Self::role_message_id(role, form)
160 && let Some(resolved) = self.resolve_message_text(
161 message_id,
162 Some(u64::from(plural) + 1),
163 requested_gender.clone(),
164 )
165 {
166 return Some(resolved);
167 }
168
169 self.role_term(role, plural, form, requested_gender)
170 .map(ToOwned::to_owned)
171 }
172
173 pub fn resolved_role_term_neutral(
175 &self,
176 role: &ContributorRole,
177 plural: bool,
178 form: &TermForm,
179 ) -> Option<String> {
180 if let Some(message_id) = Self::role_message_id(role, form)
181 && let Some(resolved) = self.resolve_message_text(
182 message_id,
183 Some(u64::from(plural) + 1),
184 Some(GrammaticalGender::Common),
185 )
186 {
187 return Some(resolved);
188 }
189
190 self.role_term_neutral(role, plural, form)
191 .map(ToOwned::to_owned)
192 }
193
194 #[must_use]
196 pub fn resolved_role_combination_term(
197 &self,
198 roles: &[ContributorRole],
199 plural: bool,
200 form: &TermForm,
201 requested_gender: Option<GrammaticalGender>,
202 ) -> Option<String> {
203 if roles.len() < 2 {
204 return None;
205 }
206 let key = if roles.len() == 2
207 && roles.contains(&ContributorRole::Editor)
208 && roles.contains(&ContributorRole::Translator)
209 {
210 "editor-translator".to_string()
211 } else {
212 roles
213 .iter()
214 .map(ContributorRole::as_str)
215 .collect::<Vec<_>>()
216 .join("-")
217 };
218 let term = self.role_combinations.get(&key)?;
219 let simple = if plural { &term.plural } else { &term.singular };
220 let value = match *form {
221 TermForm::Long => Self::resolve_gendered_value(&simple.long, requested_gender),
222 TermForm::Short => {
223 Self::resolve_gendered_value(&simple.short, requested_gender.clone())
224 .filter(|value| !value.is_empty())
225 .or_else(|| Self::resolve_gendered_value(&simple.long, requested_gender))
226 }
227 TermForm::Verb => Self::resolve_gendered_value(&term.verb.long, requested_gender),
228 TermForm::VerbShort => {
229 Self::resolve_gendered_value(&term.verb.short, requested_gender.clone())
230 .filter(|value| !value.is_empty())
231 .or_else(|| Self::resolve_gendered_value(&term.verb.long, requested_gender))
232 }
233 _ => Self::resolve_gendered_value(&simple.long, requested_gender),
234 };
235 value
236 .filter(|value| !value.is_empty())
237 .map(ToOwned::to_owned)
238 }
239
240 #[must_use]
242 pub fn role_conjunction(&self) -> &str {
243 self.general_term(&GeneralTerm::RoleConjunction, &TermForm::Long, None)
244 .unwrap_or(" & ")
245 }
246
247 pub fn locator_term(
249 &self,
250 locator: &LocatorType,
251 plural: bool,
252 form: &TermForm,
253 requested_gender: Option<GrammaticalGender>,
254 ) -> Option<&str> {
255 let term = self.locators.get(locator)?;
256 let form_term = match *form {
257 TermForm::Long => &term.long,
258 TermForm::Short => &term.short,
259 TermForm::Symbol => &term.symbol,
260 _ => &term.short, };
262
263 if let Some(ft) = form_term {
264 let value = if plural { &ft.plural } else { &ft.singular };
265 Self::resolve_gendered_value(value, requested_gender)
266 } else {
267 None
268 }
269 }
270
271 pub fn resolved_locator_term(
273 &self,
274 locator: &LocatorType,
275 plural: bool,
276 form: &TermForm,
277 requested_gender: Option<GrammaticalGender>,
278 ) -> Option<String> {
279 if let Some(message_id) = Self::locator_message_id(locator, form)
280 && let Some(resolved) = self.resolve_message_text(
281 message_id,
282 Some(u64::from(plural) + 1),
283 requested_gender.clone(),
284 )
285 {
286 return Some(resolved);
287 }
288
289 self.locator_term(locator, plural, form, requested_gender.clone())
290 .map(ToOwned::to_owned)
291 .or_else(|| {
292 if let LocatorType::Custom(key) = locator {
293 self.locator_term_any_form(locator, plural, requested_gender)
294 .map(ToOwned::to_owned)
295 .or_else(|| Some(key.clone()))
296 } else {
297 None
298 }
299 })
300 }
301
302 fn locator_term_any_form(
303 &self,
304 locator: &LocatorType,
305 plural: bool,
306 requested_gender: Option<GrammaticalGender>,
307 ) -> Option<&str> {
308 let term = self.locators.get(locator)?;
309 [&term.long, &term.short, &term.symbol]
310 .into_iter()
311 .flatten()
312 .next()
313 .map(|forms| {
314 if plural {
315 Self::resolve_gendered_value(&forms.plural, requested_gender).unwrap_or("")
316 } else {
317 Self::resolve_gendered_value(&forms.singular, requested_gender).unwrap_or("")
318 }
319 })
320 .filter(|value| !value.is_empty())
321 }
322
323 pub fn general_term(
325 &self,
326 term: &GeneralTerm,
327 form: &TermForm,
328 requested_gender: Option<GrammaticalGender>,
329 ) -> Option<&str> {
330 if let Some(candidate_id) = Self::general_message_id(term, form)
338 && let Some(msg) = self.messages.get(candidate_id)
339 {
340 if !msg.contains('{') {
342 return Some(msg.as_str());
343 }
344 }
345 let legacy_key = Self::general_term_to_legacy_key(term);
347 if let Some(msg_id) = self.legacy_term_aliases.get(legacy_key)
348 && let Some(msg) = self.messages.get(msg_id)
349 && !msg.contains('{')
350 {
351 return Some(msg.as_str());
352 }
353
354 if *term != GeneralTerm::NoDate
356 && let Some(simple) = self.terms.general.get(term)
357 {
358 return match *form {
359 TermForm::Long => Self::resolve_gendered_value(&simple.long, requested_gender),
360 TermForm::Short => {
361 Self::resolve_gendered_value(&simple.short, requested_gender.clone())
362 .filter(|value| !value.is_empty())
363 .or_else(|| Self::resolve_gendered_value(&simple.long, requested_gender))
364 }
365
366 _ => Self::resolve_gendered_value(&simple.long, requested_gender),
367 };
368 }
369
370 match term {
372 GeneralTerm::And => self.terms.and.as_deref(),
373 GeneralTerm::EtAl => self.terms.et_al.as_deref(),
374 GeneralTerm::AndOthers => self.terms.and_others.as_deref(),
375 GeneralTerm::Accessed => self.terms.accessed.as_deref(),
376 GeneralTerm::Ibid => self.terms.ibid.as_deref(),
377 GeneralTerm::In => self.terms.in_.as_deref(),
378 GeneralTerm::NoDate => self
379 .terms
380 .general
381 .get(term)
382 .and_then(|value| Self::resolve_no_date_value(value, form, requested_gender))
383 .or(self.terms.no_date.as_deref()),
384 GeneralTerm::Retrieved => self.terms.retrieved.as_deref(),
385 GeneralTerm::At => self.terms.at.as_deref(),
386 GeneralTerm::By => self.terms.by.as_deref(),
387 GeneralTerm::From => self.terms.from.as_deref(),
388 GeneralTerm::Of => self
389 .terms
390 .general
391 .get(term)
392 .and_then(|value| Self::resolve_gendered_value(&value.long, requested_gender)),
393 GeneralTerm::To => self
394 .terms
395 .general
396 .get(term)
397 .and_then(|value| Self::resolve_gendered_value(&value.long, requested_gender)),
398 GeneralTerm::Anonymous => {
399 Self::resolve_gendered_value(&self.terms.anonymous.long, requested_gender)
400 }
401 GeneralTerm::Circa => {
402 Self::resolve_gendered_value(&self.terms.circa.long, requested_gender)
403 }
404 GeneralTerm::Volume => {
406 self.locator_term(&LocatorType::Volume, false, form, requested_gender)
407 }
408 GeneralTerm::Issue => {
409 self.locator_term(&LocatorType::Issue, false, form, requested_gender)
410 }
411 GeneralTerm::Page => {
412 self.locator_term(&LocatorType::Page, false, form, requested_gender)
413 }
414 GeneralTerm::Chapter => {
415 self.locator_term(&LocatorType::Chapter, false, form, requested_gender)
416 }
417 GeneralTerm::Section => {
418 self.locator_term(&LocatorType::Section, false, form, requested_gender)
419 }
420 GeneralTerm::Here => self
421 .terms
422 .general
423 .get(term)
424 .and_then(|value| Self::resolve_gendered_value(&value.long, requested_gender)),
425 GeneralTerm::Deposited => self
426 .terms
427 .general
428 .get(term)
429 .and_then(|value| Self::resolve_gendered_value(&value.long, requested_gender)),
430 _ => None,
431 }
432 }
433
434 pub fn resolved_general_term(
436 &self,
437 term: &GeneralTerm,
438 form: &TermForm,
439 requested_gender: Option<GrammaticalGender>,
440 ) -> Option<String> {
441 if let Some(message_id) = Self::general_message_id(term, form)
442 && let Some(resolved) =
443 self.resolve_message_text(message_id, None, requested_gender.clone())
444 {
445 return Some(resolved);
446 }
447
448 self.general_term(term, form, requested_gender)
449 .map(ToOwned::to_owned)
450 }
451
452 #[must_use]
460 pub fn resolved_type_term(&self, ref_type: &str, form: &TermForm) -> Option<String> {
461 let simple = self.type_terms.get(ref_type)?;
462 match form {
463 TermForm::Short => Self::resolve_gendered_value(&simple.short, None)
464 .filter(|value| !value.is_empty())
465 .or_else(|| Self::resolve_gendered_value(&simple.long, None)),
466 _ => Self::resolve_gendered_value(&simple.long, None),
467 }
468 .map(ToOwned::to_owned)
469 }
470
471 pub fn resolved_archive_term(&self, field: ArchiveHierarchyField) -> Option<String> {
474 self.resolve_message_text(field.message_id(), Some(1), None)
475 }
476
477 pub fn and_term(&self, use_symbol: bool) -> &str {
479 if use_symbol {
480 self.terms.and_symbol.as_deref().unwrap_or("&")
481 } else {
482 self.terms.and.as_deref().unwrap_or("and")
483 }
484 }
485
486 pub fn et_al(&self) -> &str {
488 self.terms.et_al.as_deref().unwrap_or("et al.")
489 }
490
491 pub fn month_name(&self, month: u8, short: bool) -> &str {
493 let idx = (month.saturating_sub(1)) as usize;
494 if short {
495 self.dates
496 .months
497 .short
498 .get(idx)
499 .map(|s| s.as_str())
500 .unwrap_or("")
501 } else {
502 self.dates
503 .months
504 .long
505 .get(idx)
506 .map(|s| s.as_str())
507 .unwrap_or("")
508 }
509 }
510}
511
512#[cfg(test)]
513#[allow(
514 clippy::unwrap_used,
515 clippy::expect_used,
516 clippy::panic,
517 clippy::indexing_slicing,
518 clippy::todo,
519 clippy::unimplemented,
520 clippy::unreachable,
521 clippy::get_unwrap,
522 reason = "Panicking is acceptable and often desired in tests."
523)]
524mod tests {
525 use super::*;
526
527 #[test]
528 fn test_en_us_locale_terms() {
529 let locale = Locale::en_us();
530 assert_eq!(locale.and_term(false), "and");
531 assert_eq!(locale.and_term(true), "&");
532 assert_eq!(locale.et_al(), "et al.");
533 }
534
535 #[test]
536 fn test_month_names() {
537 let locale = Locale::en_us();
538 assert_eq!(locale.month_name(1, false), "January");
539 assert_eq!(locale.month_name(1, true), "Jan.");
540 assert_eq!(locale.month_name(12, false), "December");
541 }
542
543 #[test]
544 fn test_role_terms() {
545 let locale = Locale::en_us();
546
547 assert_eq!(
548 locale.role_term(&ContributorRole::Editor, false, &TermForm::Short, None),
549 Some("ed.")
550 );
551 assert_eq!(
552 locale.role_term(&ContributorRole::Editor, true, &TermForm::Short, None),
553 Some("eds.")
554 );
555 assert_eq!(
556 locale.role_term(&ContributorRole::Translator, false, &TermForm::Verb, None),
557 Some("translated by")
558 );
559 }
560
561 #[test]
562 fn test_no_date_term_resolves_long_and_short_forms() {
563 let locale = Locale::en_us();
564
565 assert_eq!(
566 locale.general_term(&GeneralTerm::NoDate, &TermForm::Long, None),
567 Some("no date")
568 );
569 assert_eq!(
570 locale.general_term(&GeneralTerm::NoDate, &TermForm::Short, None),
571 Some("n.d.")
572 );
573 }
574
575 #[test]
576 fn test_no_date_term_falls_back_to_legacy_short_form() {
577 let mut locale = Locale::default();
578 locale.terms.no_date = Some("n.d.".to_string());
579
580 assert_eq!(
581 locale.general_term(&GeneralTerm::NoDate, &TermForm::Short, None),
582 Some("n.d.")
583 );
584 assert_eq!(
585 locale.general_term(&GeneralTerm::NoDate, &TermForm::Long, None),
586 Some("n.d.")
587 );
588 }
589
590 #[test]
591 fn test_yaml_no_date_term_preserves_long_and_short_forms() {
592 let yaml = r#"
593locale: en-US
594dates:
595 months:
596 long: [January, February, March, April, May, June, July, August, September, October, November, December]
597 short: [Jan., Feb., Mar., Apr., May, June, July, Aug., Sept., Oct., Nov., Dec.]
598 seasons: [Spring, Summer, Autumn, Winter]
599roles: {}
600terms:
601 no date:
602 long: no date
603 short: n.d.
604"#;
605
606 let locale = Locale::from_yaml_str(yaml).unwrap();
607 assert_eq!(
608 locale.general_term(&GeneralTerm::NoDate, &TermForm::Long, None),
609 Some("no date")
610 );
611 assert_eq!(
612 locale.general_term(&GeneralTerm::NoDate, &TermForm::Short, None),
613 Some("n.d.")
614 );
615 assert_eq!(locale.terms.no_date.as_deref(), Some("n.d."));
616 }
617
618 #[test]
619 fn test_resolved_locator_term_evaluates_plural_message() {
620 let locale = Locale::en_us();
621
622 assert_eq!(
623 locale.resolved_locator_term(&LocatorType::Page, false, &TermForm::Short, None),
624 Some("p.".to_string())
625 );
626 assert_eq!(
627 locale.resolved_locator_term(&LocatorType::Page, true, &TermForm::Short, None),
628 Some("pp.".to_string())
629 );
630 }
631
632 #[test]
633 fn test_resolved_locator_term_falls_back_to_custom_locale_form_then_raw_key() {
634 let locale = Locale::from_yaml_str(
635 r#"
636locale: en-US
637locators:
638 reel:
639 long:
640 singular: "reel"
641 plural: "reels"
642"#,
643 )
644 .expect("custom locale should parse");
645
646 assert_eq!(
647 locale.resolved_locator_term(
648 &LocatorType::Custom("reel".to_string()),
649 false,
650 &TermForm::Short,
651 None,
652 ),
653 Some("reel".to_string())
654 );
655 assert_eq!(
656 locale.resolved_locator_term(
657 &LocatorType::Custom("movement".to_string()),
658 false,
659 &TermForm::Short,
660 None,
661 ),
662 Some("movement".to_string())
663 );
664 }
665
666 #[test]
667 fn test_legacy_locator_terms_under_terms_still_populate_locators() {
668 let locale = Locale::from_yaml_str(
669 r#"
670locale: en-US
671terms:
672 page:
673 short:
674 singular: "pg."
675 plural: "pgs."
676"#,
677 )
678 .expect("legacy locator terms should parse");
679
680 assert_eq!(
681 locale.resolved_locator_term(&LocatorType::Page, false, &TermForm::Short, None),
682 Some("pg.".to_string())
683 );
684 }
685
686 #[test]
687 fn test_explicit_locators_override_legacy_terms_for_builtin_keys() {
688 let locale = Locale::from_yaml_str(
689 r#"
690locale: en-US
691terms:
692 page:
693 short:
694 singular: "pg."
695 plural: "pgs."
696locators:
697 page:
698 short:
699 singular: "p."
700 plural: "pp."
701"#,
702 )
703 .expect("mixed locator forms should parse");
704
705 assert_eq!(
706 locale.resolved_locator_term(&LocatorType::Page, false, &TermForm::Short, None),
707 Some("p.".to_string())
708 );
709 }
710
711 #[test]
712 fn test_non_locator_terms_are_not_reclassified_as_custom_locators() {
713 let locale = Locale::from_yaml_str(
714 r#"
715locale: en-US
716terms:
717 and:
718 long: "und"
719"#,
720 )
721 .expect("general terms should parse");
722
723 assert_eq!(locale.terms.and.as_deref(), Some("und"));
724 assert!(
725 !locale
726 .locators
727 .contains_key(&LocatorType::Custom("and".to_string()))
728 );
729 }
730
731 #[test]
732 fn test_resolved_role_term_evaluates_plural_message() {
733 let locale = Locale::en_us();
734
735 assert_eq!(
736 locale.resolved_role_term(&ContributorRole::Editor, false, &TermForm::Long, None),
737 Some("editor".to_string())
738 );
739 assert_eq!(
740 locale.resolved_role_term(&ContributorRole::Editor, true, &TermForm::Long, None),
741 Some("editors".to_string())
742 );
743 }
744
745 #[test]
746 fn test_role_term_prefers_common_form_for_mixed_gender_requests() {
747 let locale = Locale::from_yaml_str(
748 r#"
749locale: es-ES
750roles:
751 editor:
752 long:
753 singular:
754 masculine: editor
755 feminine: editora
756 common: persona editora
757 plural:
758 masculine: editores
759 feminine: editoras
760 common: equipo editorial
761 short:
762 singular: ed.
763 plural: eds.
764 verb: editado por
765"#,
766 )
767 .expect("gendered locale should parse");
768
769 assert_eq!(
770 locale.role_term(
771 &ContributorRole::Editor,
772 false,
773 &TermForm::Long,
774 Some(GrammaticalGender::Feminine),
775 ),
776 Some("editora")
777 );
778 assert_eq!(
779 locale.role_term(
780 &ContributorRole::Editor,
781 true,
782 &TermForm::Long,
783 Some(GrammaticalGender::Common),
784 ),
785 Some("equipo editorial")
786 );
787 }
788
789 #[test]
790 fn test_no_date_term_falls_back_when_requested_gender_has_no_matching_slot() {
791 let locale = Locale::from_yaml_str(
792 r#"
793locale: es-ES
794terms:
795 no date:
796 long:
797 masculine: sin fecha
798 no_date: s. f.
799"#,
800 )
801 .expect("locale should parse");
802
803 assert_eq!(
804 locale.general_term(
805 &GeneralTerm::NoDate,
806 &TermForm::Long,
807 Some(GrammaticalGender::Common),
808 ),
809 Some("s. f.")
810 );
811 }
812
813 #[test]
814 fn test_es_es_locale_is_embedded() {
815 let bytes = crate::embedded::get_locale_bytes("es-ES").expect("es-ES should be embedded");
816 let yaml = std::str::from_utf8(bytes).expect("embedded locale should be utf-8");
817 let locale = Locale::from_yaml_str(yaml).expect("embedded es-ES should parse");
818
819 assert_eq!(locale.locale, "es-ES");
820 assert_eq!(
821 locale.resolved_role_term(
822 &ContributorRole::Editor,
823 false,
824 &TermForm::Long,
825 Some(GrammaticalGender::Feminine),
826 ),
827 Some("editora".to_string())
828 );
829 }
830
831 #[test]
832 fn test_es_es_role_term_resolves_gendered_mf2_message() {
833 let bytes = crate::embedded::get_locale_bytes("es-ES").expect("es-ES should be embedded");
834 let yaml = std::str::from_utf8(bytes).expect("embedded locale should be utf-8");
835 let locale = Locale::from_yaml_str(yaml).expect("embedded es-ES should parse");
836
837 assert_eq!(
838 locale.resolved_role_term(
839 &ContributorRole::Editor,
840 true,
841 &TermForm::Long,
842 Some(GrammaticalGender::Masculine),
843 ),
844 Some("editores".to_string())
845 );
846 assert_eq!(
847 locale.resolved_role_term(
848 &ContributorRole::Translator,
849 true,
850 &TermForm::Long,
851 Some(GrammaticalGender::Feminine),
852 ),
853 Some("traductoras".to_string())
854 );
855 assert_eq!(
856 locale.resolved_role_term_neutral(&ContributorRole::Editor, true, &TermForm::Long),
857 Some("equipo editorial".to_string())
858 );
859 }
860
861 #[test]
862 fn test_role_term_falls_back_when_mf2_message_cannot_evaluate() {
863 let locale = Locale::from_yaml_str(
864 r#"
865locale: es-ES
866evaluation:
867 message-syntax: mf2
868messages:
869 role.editor.label-long: |
870 .match {$gender :unknown} {$count :plural}
871 when feminine one {editora}
872roles:
873 editor:
874 long:
875 singular:
876 feminine: editora heredada
877 plural:
878 feminine: editoras heredadas
879"#,
880 )
881 .expect("locale should parse");
882
883 assert_eq!(
884 locale.resolved_role_term(
885 &ContributorRole::Editor,
886 false,
887 &TermForm::Long,
888 Some(GrammaticalGender::Feminine),
889 ),
890 Some("editora heredada".to_string())
891 );
892 }
893}