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 {
494 let Some(code) = super::types::SubYearCode::new(month) else {
495 return "";
496 };
497 let table = if short {
498 &self.dates.months.short
499 } else {
500 &self.dates.months.long
501 };
502 table.get(&code).map(String::as_str).unwrap_or("")
503 }
504}
505
506#[cfg(test)]
507#[allow(
508 clippy::unwrap_used,
509 clippy::expect_used,
510 clippy::panic,
511 clippy::indexing_slicing,
512 clippy::todo,
513 clippy::unimplemented,
514 clippy::unreachable,
515 clippy::get_unwrap,
516 reason = "Panicking is acceptable and often desired in tests."
517)]
518mod tests {
519 use super::*;
520
521 #[test]
522 fn test_en_us_locale_terms() {
523 let locale = Locale::en_us();
524 assert_eq!(locale.and_term(false), "and");
525 assert_eq!(locale.and_term(true), "&");
526 assert_eq!(locale.et_al(), "et al.");
527 }
528
529 #[test]
530 fn test_month_names() {
531 let locale = Locale::en_us();
532 assert_eq!(locale.month_name(1, false), "January");
533 assert_eq!(locale.month_name(1, true), "Jan.");
534 assert_eq!(locale.month_name(12, false), "December");
535 }
536
537 #[test]
538 fn test_role_terms() {
539 let locale = Locale::en_us();
540
541 assert_eq!(
542 locale.role_term(&ContributorRole::Editor, false, &TermForm::Short, None),
543 Some("ed.")
544 );
545 assert_eq!(
546 locale.role_term(&ContributorRole::Editor, true, &TermForm::Short, None),
547 Some("eds.")
548 );
549 assert_eq!(
550 locale.role_term(&ContributorRole::Translator, false, &TermForm::Verb, None),
551 Some("translated by")
552 );
553 }
554
555 #[test]
556 fn test_no_date_term_resolves_long_and_short_forms() {
557 let locale = Locale::en_us();
558
559 assert_eq!(
560 locale.general_term(&GeneralTerm::NoDate, &TermForm::Long, None),
561 Some("no date")
562 );
563 assert_eq!(
564 locale.general_term(&GeneralTerm::NoDate, &TermForm::Short, None),
565 Some("n.d.")
566 );
567 }
568
569 #[test]
570 fn test_no_date_term_falls_back_to_legacy_short_form() {
571 let mut locale = Locale::default();
572 locale.terms.no_date = Some("n.d.".to_string());
573
574 assert_eq!(
575 locale.general_term(&GeneralTerm::NoDate, &TermForm::Short, None),
576 Some("n.d.")
577 );
578 assert_eq!(
579 locale.general_term(&GeneralTerm::NoDate, &TermForm::Long, None),
580 Some("n.d.")
581 );
582 }
583
584 #[test]
585 fn test_yaml_no_date_term_preserves_long_and_short_forms() {
586 let yaml = r#"
587locale: en-US
588dates:
589 months:
590 long: [January, February, March, April, May, June, July, August, September, October, November, December]
591 short: [Jan., Feb., Mar., Apr., May, June, July, Aug., Sept., Oct., Nov., Dec.]
592 seasons: [Spring, Summer, Autumn, Winter]
593roles: {}
594terms:
595 no date:
596 long: no date
597 short: n.d.
598"#;
599
600 let locale = Locale::from_yaml_str(yaml).unwrap();
601 assert_eq!(
602 locale.general_term(&GeneralTerm::NoDate, &TermForm::Long, None),
603 Some("no date")
604 );
605 assert_eq!(
606 locale.general_term(&GeneralTerm::NoDate, &TermForm::Short, None),
607 Some("n.d.")
608 );
609 assert_eq!(locale.terms.no_date.as_deref(), Some("n.d."));
610 }
611
612 #[test]
613 fn test_resolved_locator_term_evaluates_plural_message() {
614 let locale = Locale::en_us();
615
616 assert_eq!(
617 locale.resolved_locator_term(&LocatorType::Page, false, &TermForm::Short, None),
618 Some("p.".to_string())
619 );
620 assert_eq!(
621 locale.resolved_locator_term(&LocatorType::Page, true, &TermForm::Short, None),
622 Some("pp.".to_string())
623 );
624 }
625
626 #[test]
627 fn test_resolved_locator_term_falls_back_to_custom_locale_form_then_raw_key() {
628 let locale = Locale::from_yaml_str(
629 r#"
630locale: en-US
631locators:
632 reel:
633 long:
634 singular: "reel"
635 plural: "reels"
636"#,
637 )
638 .expect("custom locale should parse");
639
640 assert_eq!(
641 locale.resolved_locator_term(
642 &LocatorType::Custom("reel".to_string()),
643 false,
644 &TermForm::Short,
645 None,
646 ),
647 Some("reel".to_string())
648 );
649 assert_eq!(
650 locale.resolved_locator_term(
651 &LocatorType::Custom("movement".to_string()),
652 false,
653 &TermForm::Short,
654 None,
655 ),
656 Some("movement".to_string())
657 );
658 }
659
660 #[test]
661 fn test_legacy_locator_terms_under_terms_still_populate_locators() {
662 let locale = Locale::from_yaml_str(
663 r#"
664locale: en-US
665terms:
666 page:
667 short:
668 singular: "pg."
669 plural: "pgs."
670"#,
671 )
672 .expect("legacy locator terms should parse");
673
674 assert_eq!(
675 locale.resolved_locator_term(&LocatorType::Page, false, &TermForm::Short, None),
676 Some("pg.".to_string())
677 );
678 }
679
680 #[test]
681 fn test_explicit_locators_override_legacy_terms_for_builtin_keys() {
682 let locale = Locale::from_yaml_str(
683 r#"
684locale: en-US
685terms:
686 page:
687 short:
688 singular: "pg."
689 plural: "pgs."
690locators:
691 page:
692 short:
693 singular: "p."
694 plural: "pp."
695"#,
696 )
697 .expect("mixed locator forms should parse");
698
699 assert_eq!(
700 locale.resolved_locator_term(&LocatorType::Page, false, &TermForm::Short, None),
701 Some("p.".to_string())
702 );
703 }
704
705 #[test]
706 fn test_non_locator_terms_are_not_reclassified_as_custom_locators() {
707 let locale = Locale::from_yaml_str(
708 r#"
709locale: en-US
710terms:
711 and:
712 long: "und"
713"#,
714 )
715 .expect("general terms should parse");
716
717 assert_eq!(locale.terms.and.as_deref(), Some("und"));
718 assert!(
719 !locale
720 .locators
721 .contains_key(&LocatorType::Custom("and".to_string()))
722 );
723 }
724
725 #[test]
726 fn test_resolved_role_term_evaluates_plural_message() {
727 let locale = Locale::en_us();
728
729 assert_eq!(
730 locale.resolved_role_term(&ContributorRole::Editor, false, &TermForm::Long, None),
731 Some("editor".to_string())
732 );
733 assert_eq!(
734 locale.resolved_role_term(&ContributorRole::Editor, true, &TermForm::Long, None),
735 Some("editors".to_string())
736 );
737 }
738
739 #[test]
740 fn test_role_term_prefers_common_form_for_mixed_gender_requests() {
741 let locale = Locale::from_yaml_str(
742 r#"
743locale: es-ES
744roles:
745 editor:
746 long:
747 singular:
748 masculine: editor
749 feminine: editora
750 common: persona editora
751 plural:
752 masculine: editores
753 feminine: editoras
754 common: equipo editorial
755 short:
756 singular: ed.
757 plural: eds.
758 verb: editado por
759"#,
760 )
761 .expect("gendered locale should parse");
762
763 assert_eq!(
764 locale.role_term(
765 &ContributorRole::Editor,
766 false,
767 &TermForm::Long,
768 Some(GrammaticalGender::Feminine),
769 ),
770 Some("editora")
771 );
772 assert_eq!(
773 locale.role_term(
774 &ContributorRole::Editor,
775 true,
776 &TermForm::Long,
777 Some(GrammaticalGender::Common),
778 ),
779 Some("equipo editorial")
780 );
781 }
782
783 #[test]
784 fn test_no_date_term_falls_back_when_requested_gender_has_no_matching_slot() {
785 let locale = Locale::from_yaml_str(
786 r#"
787locale: es-ES
788terms:
789 no date:
790 long:
791 masculine: sin fecha
792 no_date: s. f.
793"#,
794 )
795 .expect("locale should parse");
796
797 assert_eq!(
798 locale.general_term(
799 &GeneralTerm::NoDate,
800 &TermForm::Long,
801 Some(GrammaticalGender::Common),
802 ),
803 Some("s. f.")
804 );
805 }
806
807 #[test]
808 fn test_es_es_locale_is_embedded() {
809 let bytes = crate::embedded::get_locale_bytes("es-ES").expect("es-ES should be embedded");
810 let yaml = std::str::from_utf8(bytes).expect("embedded locale should be utf-8");
811 let locale = Locale::from_yaml_str(yaml).expect("embedded es-ES should parse");
812
813 assert_eq!(locale.locale, "es-ES");
814 assert_eq!(
815 locale.resolved_role_term(
816 &ContributorRole::Editor,
817 false,
818 &TermForm::Long,
819 Some(GrammaticalGender::Feminine),
820 ),
821 Some("editora".to_string())
822 );
823 }
824
825 #[test]
826 fn test_es_es_role_term_resolves_gendered_mf2_message() {
827 let bytes = crate::embedded::get_locale_bytes("es-ES").expect("es-ES should be embedded");
828 let yaml = std::str::from_utf8(bytes).expect("embedded locale should be utf-8");
829 let locale = Locale::from_yaml_str(yaml).expect("embedded es-ES should parse");
830
831 assert_eq!(
832 locale.resolved_role_term(
833 &ContributorRole::Editor,
834 true,
835 &TermForm::Long,
836 Some(GrammaticalGender::Masculine),
837 ),
838 Some("editores".to_string())
839 );
840 assert_eq!(
841 locale.resolved_role_term(
842 &ContributorRole::Translator,
843 true,
844 &TermForm::Long,
845 Some(GrammaticalGender::Feminine),
846 ),
847 Some("traductoras".to_string())
848 );
849 assert_eq!(
850 locale.resolved_role_term_neutral(&ContributorRole::Editor, true, &TermForm::Long),
851 Some("equipo editorial".to_string())
852 );
853 }
854
855 #[test]
856 fn test_role_term_falls_back_when_mf2_message_cannot_evaluate() {
857 let locale = Locale::from_yaml_str(
858 r#"
859locale: es-ES
860evaluation:
861 message-syntax: mf2
862messages:
863 role.editor.label-long: |
864 .match {$gender :unknown} {$count :plural}
865 when feminine one {editora}
866roles:
867 editor:
868 long:
869 singular:
870 feminine: editora heredada
871 plural:
872 feminine: editoras heredadas
873"#,
874 )
875 .expect("locale should parse");
876
877 assert_eq!(
878 locale.resolved_role_term(
879 &ContributorRole::Editor,
880 false,
881 &TermForm::Long,
882 Some(GrammaticalGender::Feminine),
883 ),
884 Some("editora heredada".to_string())
885 );
886 }
887}