1use super::Locale;
14use super::message::{MessageEvaluator, Mf2MessageEvaluator, NoOpEvaluator};
15use super::raw;
16use super::types::{
17 ContributorTerm, DateTerms, LocaleOverride, LocatorTerm, MaybeGendered, MessageSyntax,
18 MonthNames, SimpleTerm, SingularPlural, TermForm,
19};
20use crate::citation::LocatorType;
21use crate::template::ContributorRole;
22use std::collections::HashMap;
23use std::sync::Arc;
24
25impl Locale {
26 pub fn from_yaml_str(yaml: &str) -> Result<Self, String> {
32 let raw: raw::RawLocale = serde_yaml::from_str(yaml)
33 .map_err(|e| format!("Failed to parse locale YAML: {}", e))?;
34
35 Ok(Self::from_raw(raw))
36 }
37
38 pub fn load(locale_id: &str, locales_dir: &std::path::Path) -> Self {
41 let extensions = ["yaml", "yml", "json", "cbor"];
42
43 for ext in &extensions {
44 let file_name = format!("{}.{}", locale_id, ext);
45 let file_path = locales_dir.join(&file_name);
46
47 if file_path.exists() {
48 match Self::from_file(&file_path) {
49 Ok(locale) => return locale,
50 Err(e) => {
51 eprintln!(
52 "Warning: Failed to load locale {}.{}: {}",
53 locale_id, ext, e
54 );
55 }
56 }
57 }
58 }
59
60 if locale_id.contains('-') {
61 let base = locale_id.split('-').next().unwrap_or("en");
62 if let Ok(entries) = std::fs::read_dir(locales_dir) {
63 for entry in entries.flatten() {
64 let name = entry.file_name();
65 let name_str = name.to_string_lossy();
66 if (name_str.starts_with(base)
67 && extensions.iter().any(|ext| name_str.ends_with(ext)))
68 && let Ok(locale) = Self::from_file(&entry.path())
69 {
70 return locale;
71 }
72 }
73 }
74 }
75
76 Self::en_us()
77 }
78
79 pub fn from_file(path: &std::path::Path) -> Result<Self, String> {
86 let bytes =
87 std::fs::read(path).map_err(|e| format!("Failed to read locale file: {}", e))?;
88 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("yaml");
89
90 match ext {
91 "cbor" => ciborium::de::from_reader::<raw::RawLocale, _>(std::io::Cursor::new(&bytes))
92 .map(Self::from_raw)
93 .map_err(|e| format!("Failed to parse CBOR locale: {}", e)),
94 "json" => serde_json::from_slice::<raw::RawLocale>(&bytes)
95 .map(Self::from_raw)
96 .map_err(|e| format!("Failed to parse JSON locale: {}", e)),
97 _ => {
98 let content = String::from_utf8_lossy(&bytes);
99 Self::from_yaml_str(&content)
100 }
101 }
102 }
103
104 fn from_raw(raw: raw::RawLocale) -> Self {
116 Self::from_raw_with_base(raw, Locale::en_us())
117 }
118
119 #[allow(
129 clippy::too_many_lines,
130 reason = "Complex parsing of raw locale data with multiple term types"
131 )]
132 pub(super) fn from_raw_with_base(raw: raw::RawLocale, base: Self) -> Self {
133 let punctuation_in_quote = raw.locale.starts_with("en-US")
134 || (raw.locale.starts_with("en") && !raw.locale.starts_with("en-GB"));
135
136 let mut locale = base;
137 locale.locale = raw.locale.clone();
138 Self::remove_base_messages_shadowed_by_raw_terms(&raw, &mut locale.messages);
139 locale.dates = DateTerms {
140 months: MonthNames {
141 long: raw.dates.months.long,
142 short: raw.dates.months.short,
143 },
144 seasons: raw.dates.seasons,
145 uncertainty_term: raw.dates.uncertainty_term,
146 open_ended_term: raw.dates.open_ended_term,
147 am: raw.dates.am,
148 pm: raw.dates.pm,
149 timezone_utc: raw.dates.timezone_utc,
150 before_era: raw.dates.before_era,
151 ad: raw.dates.ad,
152 bc: raw.dates.bc,
153 bce: raw.dates.bce,
154 ce: raw.dates.ce,
155 };
156 locale.punctuation_in_quote = punctuation_in_quote;
157 locale.sort_articles = Self::default_articles_for_locale(&raw.locale);
158
159 locale.locale_schema_version = raw.locale_schema_version;
160 locale.evaluation = raw.evaluation.unwrap_or_default();
161 locale.messages.extend(raw.messages);
162 locale.date_formats.extend(raw.date_formats);
163 locale.legacy_term_aliases.extend(raw.legacy_term_aliases);
164
165 if let Some(raw_vocab) = raw.vocab {
166 locale.vocab.genre.extend(raw_vocab.genre);
167 locale.vocab.medium.extend(raw_vocab.medium);
168 }
169
170 if let Some(go) = raw.grammar_options {
171 locale.grammar_options = go;
172 } else {
173 locale.grammar_options.punctuation_in_quote = locale.punctuation_in_quote;
174 }
175 locale.punctuation_in_quote = locale.grammar_options.punctuation_in_quote;
176 locale.punctuation_realization = raw.punctuation_realization;
177
178 if let Some(nf) = raw.number_formats {
179 locale.number_formats = nf;
180 }
181
182 let explicit_locator_keys: std::collections::HashSet<LocatorType> = raw
183 .locators
184 .keys()
185 .filter_map(|key| Self::parse_builtin_locator_type(key))
186 .collect();
187
188 for (key, value) in &raw.locators {
189 if let Some(locator_type) = Self::parse_locator_type(key) {
190 let locator_term = LocatorTerm {
191 long: Self::extract_singular_plural(value.long.as_ref().as_ref()),
192 short: Self::extract_singular_plural(value.short.as_ref().as_ref()),
193 symbol: Self::extract_singular_plural(value.symbol.as_ref().as_ref()),
194 gender: value.gender.clone(),
195 };
196 locale.locators.insert(locator_type, locator_term);
197 }
198 }
199
200 for (key, value) in &raw.terms {
201 if let Some(locator_type) = Self::parse_builtin_locator_type(key)
202 && !explicit_locator_keys.contains(&locator_type)
203 && let Some(forms) = Self::get_forms(value)
204 {
205 let locator_term = LocatorTerm {
206 long: Self::extract_singular_plural(forms.get("long").as_ref()),
207 short: Self::extract_singular_plural(forms.get("short").as_ref()),
208 symbol: Self::extract_singular_plural(forms.get("symbol").as_ref()),
209 gender: None,
210 };
211 locale.locators.insert(locator_type, locator_term);
212 continue;
213 }
214
215 match key.as_str() {
216 "and" => {
217 if let Some(forms) = Self::get_forms(value) {
218 if let Some(v) = forms.get("long").and_then(|v| v.as_string()) {
219 locale.terms.and = Some(v.to_string());
220 }
221 if let Some(v) = forms.get("symbol").and_then(|v| v.as_string()) {
222 locale.terms.and_symbol = Some(v.to_string());
223 }
224 }
225 }
226 "et_al" => {
227 if let Some(forms) = Self::get_forms(value)
228 && let Some(v) = forms.get("long").and_then(|v| v.as_string())
229 {
230 locale.terms.et_al = Some(v.to_string());
231 }
232 }
233 "and others" | "and_others" => {
234 if let Some(forms) = Self::get_forms(value)
235 && let Some(v) = forms.get("long").and_then(|v| v.as_string())
236 {
237 locale.terms.and_others = Some(v.to_string());
238 }
239 }
240 "accessed" => {
241 if let Some(forms) = Self::get_forms(value)
242 && let Some(v) = forms.get("long").and_then(|v| v.as_string())
243 {
244 locale.terms.accessed = Some(v.to_string());
245 }
246 }
247 "ibid" => {
248 if let Some(forms) = Self::get_forms(value)
249 && let Some(v) = forms.get("long").and_then(|v| v.as_string())
250 {
251 locale.terms.ibid = Some(v.to_string());
252 }
253 }
254 "no date" => {
255 let simple = Self::extract_simple_term_from_raw(value);
256 let short_fallback = simple.short.as_default_str().to_string();
257 locale
258 .terms
259 .general
260 .insert(super::types::GeneralTerm::NoDate, simple);
261 locale.terms.no_date.get_or_insert(short_fallback);
262 }
263 "no_date" => {
264 let simple = Self::extract_simple_term_from_raw(value);
265 locale.terms.no_date = Some(simple.short.as_str().to_string());
266 locale
267 .terms
268 .general
269 .entry(super::types::GeneralTerm::NoDate)
270 .or_insert(simple);
271 }
272 _ => {
273 if let Some(general_term) = Self::parse_general_term(key) {
274 let simple = Self::extract_simple_term_from_raw(value);
275 locale.terms.general.insert(general_term, simple);
276 } else {
277 let normalized = Self::normalize_term_key(key);
278 if Self::is_known_type_term_key(&normalized) {
279 let simple = Self::extract_simple_term_from_raw(value);
280 locale.type_terms.insert(normalized, simple);
281 }
282 }
283 }
284 }
285 }
286
287 for (key, role_term) in &raw.roles {
288 let contributor_term = ContributorTerm {
289 singular: Self::extract_simple_term(&role_term.long, &role_term.short, false),
290 plural: Self::extract_simple_term(&role_term.long, &role_term.short, true),
291 verb: Self::extract_verb_term(&role_term.verb, &role_term.verb_short),
292 };
293 if let Some(role) = Self::parse_role_name(key) {
294 locale.roles.insert(role, contributor_term);
295 } else {
296 let canonical = if key == "editortranslator" {
297 "editor-translator".to_string()
298 } else {
299 Self::normalize_term_key(key)
300 };
301 locale.role_combinations.insert(canonical, contributor_term);
302 }
303 }
304
305 locale.evaluator = match locale.evaluation.message_syntax {
306 MessageSyntax::Mf2 => Arc::new(Mf2MessageEvaluator) as Arc<dyn MessageEvaluator>,
307 MessageSyntax::Static => Arc::new(NoOpEvaluator),
308 };
309
310 locale
311 }
312
313 fn default_articles_for_locale(locale_id: &str) -> Vec<String> {
315 #[allow(clippy::string_slice, reason = "locale_id is expected to be ASCII")]
316 let lang = &locale_id[..2.min(locale_id.len())];
317 match lang {
318 "en" => vec!["the".into(), "a".into(), "an".into()],
319 "de" => vec![
320 "der".into(),
321 "die".into(),
322 "das".into(),
323 "ein".into(),
324 "eine".into(),
325 ],
326 "fr" => vec![
327 "le".into(),
328 "la".into(),
329 "les".into(),
330 "l'".into(),
331 "un".into(),
332 "une".into(),
333 ],
334 "es" => vec![
335 "el".into(),
336 "la".into(),
337 "los".into(),
338 "las".into(),
339 "un".into(),
340 "una".into(),
341 ],
342 "it" => vec![
343 "il".into(),
344 "lo".into(),
345 "la".into(),
346 "i".into(),
347 "gli".into(),
348 "le".into(),
349 "un".into(),
350 "una".into(),
351 ],
352 "pt" => vec![
353 "o".into(),
354 "a".into(),
355 "os".into(),
356 "as".into(),
357 "um".into(),
358 "uma".into(),
359 ],
360 "nl" => vec!["de".into(), "het".into(), "een".into()],
361 _ => vec![],
362 }
363 }
364
365 fn get_forms(value: &raw::RawTermValue) -> Option<&HashMap<String, raw::RawTermValue>> {
366 match value {
367 raw::RawTermValue::Forms(forms) => Some(forms),
368 _ => None,
369 }
370 }
371
372 fn parse_locator_type(name: &str) -> Option<LocatorType> {
373 LocatorType::from_key(name).ok()
374 }
375
376 fn parse_builtin_locator_type(name: &str) -> Option<LocatorType> {
377 match Self::parse_locator_type(name)? {
378 LocatorType::Custom(_) => None,
379 locator => Some(locator),
380 }
381 }
382
383 fn parse_role_name(name: &str) -> Option<ContributorRole> {
384 match name {
385 "author" => Some(ContributorRole::Author),
386 "chair" => Some(ContributorRole::Chair),
387 "editor" => Some(ContributorRole::Editor),
388 "translator" => Some(ContributorRole::Translator),
389 "annotator" => Some(ContributorRole::Annotator),
390 "commentator" => Some(ContributorRole::Commentator),
391 "foreword-author" => Some(ContributorRole::ForewordAuthor),
392 "introduction-author" => Some(ContributorRole::IntroductionAuthor),
393 "afterword-author" => Some(ContributorRole::AfterwordAuthor),
394 "director" => Some(ContributorRole::Director),
395 "compiler" => Some(ContributorRole::Composer),
396 "illustrator" => Some(ContributorRole::Illustrator),
397 "collection-editor" => Some(ContributorRole::CollectionEditor),
398 "container-author" => Some(ContributorRole::ContainerAuthor),
399 "editorial-director" => Some(ContributorRole::EditorialDirector),
400 "textual-editor" | "textual_editor" => Some(ContributorRole::TextualEditor),
401 "interviewer" => Some(ContributorRole::Interviewer),
402 "original-author" => Some(ContributorRole::OriginalAuthor),
403 "recipient" => Some(ContributorRole::Recipient),
404 "reviewed-author" => Some(ContributorRole::ReviewedAuthor),
405 "performer" => Some(ContributorRole::Performer),
406 "composer" => Some(ContributorRole::Composer),
407 "writer" => Some(ContributorRole::Writer),
408 "producer" => Some(ContributorRole::Producer),
409 _ => None,
410 }
411 }
412
413 fn remove_base_messages_shadowed_by_raw_terms(
414 raw: &raw::RawLocale,
415 messages: &mut HashMap<String, String>,
416 ) {
417 for key in raw.locators.keys() {
418 if let Some(locator) = Self::parse_builtin_locator_type(key) {
419 for form in [TermForm::Long, TermForm::Short] {
420 if let Some(message_id) = Self::locator_message_id(&locator, &form) {
421 messages.remove(message_id);
422 }
423 }
424 }
425 }
426
427 for key in raw.roles.keys() {
428 if let Some(role) = Self::parse_role_name(key) {
429 for form in [
430 TermForm::Long,
431 TermForm::Short,
432 TermForm::Verb,
433 TermForm::VerbShort,
434 ] {
435 if let Some(message_id) = Self::role_message_id(&role, &form) {
436 messages.remove(message_id);
437 }
438 }
439 }
440 }
441
442 for key in raw.terms.keys() {
443 if let Some(term) = Self::parse_general_term(key) {
444 for form in [TermForm::Long, TermForm::Short] {
445 if let Some(message_id) = Self::general_message_id(&term, &form) {
446 messages.remove(message_id);
447 }
448 }
449 }
450 }
451 }
452
453 fn extract_singular_plural(value: Option<&&raw::RawTermValue>) -> Option<SingularPlural> {
454 match value {
455 Some(raw::RawTermValue::SingularPlural { singular, plural }) => Some(SingularPlural {
456 singular: Self::from_raw_gendered_string(singular),
457 plural: Self::from_raw_gendered_string(plural),
458 }),
459 Some(raw::RawTermValue::Simple(s)) => Some(SingularPlural {
460 singular: MaybeGendered::Plain(s.clone()),
461 plural: MaybeGendered::Plain(s.clone()),
462 }),
463 Some(raw::RawTermValue::Gendered {
464 masculine,
465 feminine,
466 neuter,
467 common,
468 }) => Some(SingularPlural {
469 singular: MaybeGendered::Gendered {
470 masculine: masculine.clone(),
471 feminine: feminine.clone(),
472 neuter: neuter.clone(),
473 common: common.clone(),
474 },
475 plural: MaybeGendered::Gendered {
476 masculine: masculine.clone(),
477 feminine: feminine.clone(),
478 neuter: neuter.clone(),
479 common: common.clone(),
480 },
481 }),
482 Some(raw::RawTermValue::Forms(forms)) => {
483 let singular = forms
484 .get("singular")
485 .map(Self::extract_maybe_gendered_string);
486 let plural = forms.get("plural").map(Self::extract_maybe_gendered_string);
487
488 singular.map(|s| SingularPlural {
489 plural: plural.unwrap_or_else(|| s.clone()),
490 singular: s,
491 })
492 }
493 _ => None,
494 }
495 }
496
497 fn extract_simple_term(
498 long: &Option<raw::RawTermValue>,
499 short: &Option<raw::RawTermValue>,
500 plural: bool,
501 ) -> SimpleTerm {
502 let long_str = long
503 .as_ref()
504 .map(|v| Self::extract_simple_gendered_term(v, plural))
505 .unwrap_or_default();
506
507 let short_str = short
508 .as_ref()
509 .map(|v| Self::extract_simple_gendered_term(v, plural))
510 .unwrap_or_default();
511
512 SimpleTerm {
513 long: long_str,
514 short: short_str,
515 }
516 }
517
518 fn extract_verb_term(
519 verb: &Option<raw::RawTermValue>,
520 verb_short: &Option<raw::RawTermValue>,
521 ) -> SimpleTerm {
522 let long_str = verb
523 .as_ref()
524 .and_then(|v| v.as_string())
525 .unwrap_or("")
526 .into();
527
528 let short_str = verb_short
529 .as_ref()
530 .and_then(|v| v.as_string())
531 .unwrap_or("")
532 .into();
533
534 SimpleTerm {
535 long: long_str,
536 short: short_str,
537 }
538 }
539
540 fn normalize_term_key(s: &str) -> String {
548 s.replace(['_', ' '], "-")
549 }
550
551 pub fn parse_general_term(name: &str) -> Option<super::types::GeneralTerm> {
553 use super::types::GeneralTerm;
554 match Self::normalize_term_key(name).as_str() {
555 "in" => Some(GeneralTerm::In),
556 "accessed" => Some(GeneralTerm::Accessed),
557 "cited" => Some(GeneralTerm::Cited),
558 "retrieved" => Some(GeneralTerm::Retrieved),
559 "at" => Some(GeneralTerm::At),
560 "from" => Some(GeneralTerm::From),
561 "of" => Some(GeneralTerm::Of),
562 "to" => Some(GeneralTerm::To),
563 "by" => Some(GeneralTerm::By),
564 "no-date" => Some(GeneralTerm::NoDate),
565 "anonymous" => Some(GeneralTerm::Anonymous),
566 "circa" => Some(GeneralTerm::Circa),
567 "available-at" => Some(GeneralTerm::AvailableAt),
568 "ibid" => Some(GeneralTerm::Ibid),
569 "and" => Some(GeneralTerm::And),
570 "role-conjunction" => Some(GeneralTerm::RoleConjunction),
571 "et-al" => Some(GeneralTerm::EtAl),
572 "and-others" => Some(GeneralTerm::AndOthers),
573 "forthcoming" => Some(GeneralTerm::Forthcoming),
574 "online" => Some(GeneralTerm::Online),
575 "here" => Some(GeneralTerm::Here),
576 "deposited" => Some(GeneralTerm::Deposited),
577 "review-of" => Some(GeneralTerm::ReviewOf),
578 "original-work-published" => Some(GeneralTerm::OriginalWorkPublished),
579 "personal-communication" => Some(GeneralTerm::PersonalCommunication),
580 "patent" => Some(GeneralTerm::Patent),
581 "issued" => Some(GeneralTerm::Issued),
582 "volume" => Some(GeneralTerm::Volume),
583 "issue" => Some(GeneralTerm::Issue),
584 "page" => Some(GeneralTerm::Page),
585 "chapter" => Some(GeneralTerm::Chapter),
586 "edition" => Some(GeneralTerm::Edition),
587 "section" => Some(GeneralTerm::Section),
588 "version" => Some(GeneralTerm::Version),
589 _ => None,
590 }
591 }
592
593 fn is_known_type_term_key(normalized_key: &str) -> bool {
619 const KNOWN_TYPE_TERM_KEYS: &[&str] = &[
620 "article-journal",
621 "article-magazine",
622 "article-newspaper",
623 "broadcast",
624 "classic",
625 "collection",
626 "dataset",
627 "document",
628 "entry",
629 "entry-dictionary",
630 "entry-encyclopedia",
631 "event",
632 "graphic",
633 "hearing",
634 "interview",
635 "legal-case",
636 "legislation",
637 "manuscript",
638 "map",
639 "motion-picture",
640 "musical-score",
641 "pamphlet",
642 "paper-conference",
643 "performance",
644 "periodical",
645 "personal-communication",
646 "post",
647 "post-weblog",
648 "preprint",
649 "regulation",
650 "report",
651 "review",
652 "review-book",
653 "software",
654 "song",
655 "speech",
656 "standard",
657 "thesis",
658 "treaty",
659 "webpage",
660 ];
661 KNOWN_TYPE_TERM_KEYS.contains(&normalized_key)
662 }
663
664 fn extract_simple_term_from_raw(value: &raw::RawTermValue) -> SimpleTerm {
665 match value {
666 raw::RawTermValue::Simple(s) => SimpleTerm {
667 long: s.clone().into(),
668 short: s.clone().into(),
669 },
670 raw::RawTermValue::Gendered {
671 masculine,
672 feminine,
673 neuter,
674 common,
675 } => SimpleTerm {
676 long: MaybeGendered::Gendered {
677 masculine: masculine.clone(),
678 feminine: feminine.clone(),
679 neuter: neuter.clone(),
680 common: common.clone(),
681 },
682 short: MaybeGendered::Gendered {
683 masculine: masculine.clone(),
684 feminine: feminine.clone(),
685 neuter: neuter.clone(),
686 common: common.clone(),
687 },
688 },
689 raw::RawTermValue::Forms(forms) => {
690 let long = forms
691 .get("long")
692 .map(Self::extract_maybe_gendered_string)
693 .unwrap_or_default();
694 let short = forms
695 .get("short")
696 .map(Self::extract_maybe_gendered_string)
697 .unwrap_or_else(|| long.clone());
698 SimpleTerm { long, short }
699 }
700 raw::RawTermValue::SingularPlural { singular, .. } => SimpleTerm {
701 long: Self::from_raw_gendered_string(singular),
702 short: Self::from_raw_gendered_string(singular),
703 },
704 }
705 }
706
707 fn from_raw_gendered_string(value: &raw::RawGenderedString) -> MaybeGendered<String> {
708 match value {
709 raw::RawGenderedString::Simple(value) => MaybeGendered::Plain(value.clone()),
710 raw::RawGenderedString::Gendered {
711 masculine,
712 feminine,
713 neuter,
714 common,
715 } => MaybeGendered::Gendered {
716 masculine: masculine.clone(),
717 feminine: feminine.clone(),
718 neuter: neuter.clone(),
719 common: common.clone(),
720 },
721 }
722 }
723
724 fn extract_maybe_gendered_string(value: &raw::RawTermValue) -> MaybeGendered<String> {
725 match value {
726 raw::RawTermValue::Simple(value) => MaybeGendered::Plain(value.clone()),
727 raw::RawTermValue::Gendered {
728 masculine,
729 feminine,
730 neuter,
731 common,
732 } => MaybeGendered::Gendered {
733 masculine: masculine.clone(),
734 feminine: feminine.clone(),
735 neuter: neuter.clone(),
736 common: common.clone(),
737 },
738 raw::RawTermValue::SingularPlural { singular, .. } => {
739 Self::from_raw_gendered_string(singular)
740 }
741 raw::RawTermValue::Forms(forms) => forms
742 .get("long")
743 .or_else(|| forms.get("singular"))
744 .map(Self::extract_maybe_gendered_string)
745 .unwrap_or_default(),
746 }
747 }
748
749 fn extract_simple_gendered_term(
750 value: &raw::RawTermValue,
751 plural: bool,
752 ) -> MaybeGendered<String> {
753 match value {
754 raw::RawTermValue::Simple(value) => MaybeGendered::Plain(value.clone()),
755 raw::RawTermValue::Gendered {
756 masculine,
757 feminine,
758 neuter,
759 common,
760 } => MaybeGendered::Gendered {
761 masculine: masculine.clone(),
762 feminine: feminine.clone(),
763 neuter: neuter.clone(),
764 common: common.clone(),
765 },
766 raw::RawTermValue::SingularPlural {
767 singular,
768 plural: plural_value,
769 } => {
770 if plural {
771 Self::from_raw_gendered_string(plural_value)
772 } else {
773 Self::from_raw_gendered_string(singular)
774 }
775 }
776 raw::RawTermValue::Forms(forms) => {
777 let key = if plural { "plural" } else { "singular" };
778 forms
779 .get(key)
780 .or_else(|| forms.get("long"))
781 .map(Self::extract_maybe_gendered_string)
782 .unwrap_or_default()
783 }
784 }
785 }
786
787 pub fn apply_override(&mut self, ov: &LocaleOverride) {
795 for (k, v) in &ov.messages {
796 self.messages.insert(k.clone(), v.clone());
797 }
798 if let Some(go) = &ov.grammar_options {
799 self.grammar_options = go.clone();
800 self.punctuation_in_quote = go.punctuation_in_quote;
801 }
802 for (k, v) in &ov.legacy_term_aliases {
803 self.legacy_term_aliases.insert(k.clone(), v.clone());
804 }
805 }
806}