1use std::collections::BTreeMap;
2use std::ptr;
3
4use ferrocat_icu::{IcuFormatter, IcuFormatterSupport};
5
6use crate::diagnostic_codes::DiagnosticCode;
7
8use super::{
9 ApiError, CatalogMessageKey, CatalogSemantics, IcuSyntaxPolicy, NormalizedParsedCatalog,
10 compile::{
11 compiled_catalog_translation_kind_for_message, compiled_key_for,
12 describe_compiled_id_catalogs,
13 },
14};
15
16#[cfg(feature = "serde")]
17use serde::{Deserialize, Deserializer, Serialize, Serializer};
18
19pub const COMPILED_CATALOG_ARTIFACT_SCHEMA_VERSION: u16 = 1;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
25#[cfg_attr(
26 feature = "serde",
27 serde(tag = "kind", content = "value", rename_all = "snake_case")
28)]
29pub enum CompiledTranslation {
30 Singular(String),
32 Plural(BTreeMap<String, String>),
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
42#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
43#[non_exhaustive]
44pub enum CompiledKeyStrategy {
45 #[default]
49 FerrocatV1,
50}
51
52pub type IcuFormatterSupportPolicy = fn(&IcuFormatter) -> IcuFormatterSupport;
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64#[non_exhaustive]
65pub struct CompileCatalogOptions<'a> {
66 pub key_strategy: CompiledKeyStrategy,
68 pub source_fallback: bool,
70 pub source_locale: Option<&'a str>,
72 pub semantics: CatalogSemantics,
74}
75
76impl Default for CompileCatalogOptions<'_> {
77 fn default() -> Self {
78 Self {
79 key_strategy: CompiledKeyStrategy::FerrocatV1,
80 source_fallback: false,
81 source_locale: None,
82 semantics: CatalogSemantics::IcuNative,
83 }
84 }
85}
86
87impl<'a> CompileCatalogOptions<'a> {
88 #[must_use]
90 pub fn new() -> Self {
91 Self::default()
92 }
93
94 #[must_use]
96 pub fn with_key_strategy(mut self, key_strategy: CompiledKeyStrategy) -> Self {
97 self.key_strategy = key_strategy;
98 self
99 }
100
101 #[must_use]
103 pub fn with_source_fallback(mut self, source_fallback: bool) -> Self {
104 self.source_fallback = source_fallback;
105 self
106 }
107
108 #[must_use]
110 pub fn with_source_locale(mut self, source_locale: &'a str) -> Self {
111 self.source_locale = Some(source_locale);
112 self
113 }
114
115 #[must_use]
117 pub fn with_semantics(mut self, semantics: CatalogSemantics) -> Self {
118 self.semantics = semantics;
119 self
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
125#[non_exhaustive]
126pub struct CompileCatalogArtifactOptions<'a> {
127 pub requested_locale: &'a str,
129 pub source_locale: &'a str,
131 pub fallback_chain: &'a [&'a str],
133 pub key_strategy: CompiledKeyStrategy,
135 pub source_fallback: bool,
137 pub strict_icu: bool,
139 pub icu_compatibility: bool,
141 pub semantics: CatalogSemantics,
143 pub icu_options: CompileCatalogArtifactIcuOptions,
145}
146
147impl<'a> CompileCatalogArtifactOptions<'a> {
148 #[must_use]
154 pub fn new(requested_locale: &'a str, source_locale: &'a str) -> Self {
155 Self {
156 requested_locale,
157 source_locale,
158 fallback_chain: &[],
159 key_strategy: CompiledKeyStrategy::FerrocatV1,
160 source_fallback: false,
161 strict_icu: false,
162 icu_compatibility: false,
163 semantics: CatalogSemantics::IcuNative,
164 icu_options: CompileCatalogArtifactIcuOptions::new(),
165 }
166 }
167
168 #[must_use]
170 pub fn with_requested_locale(mut self, requested_locale: &'a str) -> Self {
171 self.requested_locale = requested_locale;
172 self
173 }
174
175 #[must_use]
177 pub fn with_source_locale(mut self, source_locale: &'a str) -> Self {
178 self.source_locale = source_locale;
179 self
180 }
181
182 #[must_use]
184 pub fn with_fallback_chain(mut self, fallback_chain: &'a [&'a str]) -> Self {
185 self.fallback_chain = fallback_chain;
186 self
187 }
188
189 #[must_use]
191 pub fn with_key_strategy(mut self, key_strategy: CompiledKeyStrategy) -> Self {
192 self.key_strategy = key_strategy;
193 self
194 }
195
196 #[must_use]
198 pub fn with_source_fallback(mut self, source_fallback: bool) -> Self {
199 self.source_fallback = source_fallback;
200 self
201 }
202
203 #[must_use]
205 pub fn with_strict_icu(mut self, strict_icu: bool) -> Self {
206 self.strict_icu = strict_icu;
207 self
208 }
209
210 #[must_use]
212 pub fn with_icu_compatibility(mut self, icu_compatibility: bool) -> Self {
213 self.icu_compatibility = icu_compatibility;
214 self
215 }
216
217 #[must_use]
219 pub fn with_semantics(mut self, semantics: CatalogSemantics) -> Self {
220 self.semantics = semantics;
221 self
222 }
223
224 #[must_use]
226 pub fn with_icu_options(mut self, icu_options: CompileCatalogArtifactIcuOptions) -> Self {
227 self.icu_options = icu_options;
228 self
229 }
230}
231
232#[derive(Debug, Clone, Copy, Default)]
234#[non_exhaustive]
235pub struct CompileCatalogArtifactIcuOptions {
236 pub syntax_policy: IcuSyntaxPolicy,
238 pub formatter_support: Option<IcuFormatterSupportPolicy>,
240}
241
242impl PartialEq for CompileCatalogArtifactIcuOptions {
243 fn eq(&self, other: &Self) -> bool {
244 self.syntax_policy == other.syntax_policy
245 && match (self.formatter_support, other.formatter_support) {
246 (Some(left), Some(right)) => ptr::fn_addr_eq(left, right),
251 (None, None) => true,
252 _ => false,
253 }
254 }
255}
256
257impl Eq for CompileCatalogArtifactIcuOptions {}
258
259impl CompileCatalogArtifactIcuOptions {
260 #[must_use]
262 pub fn new() -> Self {
263 Self::default()
264 }
265
266 #[must_use]
268 pub fn with_syntax_policy(mut self, syntax_policy: IcuSyntaxPolicy) -> Self {
269 self.syntax_policy = syntax_policy;
270 self
271 }
272
273 #[must_use]
275 pub fn with_formatter_support(mut self, formatter_support: IcuFormatterSupportPolicy) -> Self {
276 self.formatter_support = Some(formatter_support);
277 self
278 }
279}
280
281#[derive(Debug, Clone, PartialEq, Eq)]
283#[non_exhaustive]
284pub struct CompileSelectedCatalogArtifactOptions<'a> {
285 pub compiled_ids: &'a [&'a str],
287 pub options: CompileCatalogArtifactOptions<'a>,
289}
290
291impl<'a> CompileSelectedCatalogArtifactOptions<'a> {
292 #[must_use]
297 pub fn new(
298 requested_locale: &'a str,
299 source_locale: &'a str,
300 compiled_ids: &'a [&'a str],
301 ) -> Self {
302 Self {
303 compiled_ids,
304 options: CompileCatalogArtifactOptions::new(requested_locale, source_locale),
305 }
306 }
307
308 #[must_use]
310 pub fn with_compiled_ids(mut self, compiled_ids: &'a [&'a str]) -> Self {
311 self.compiled_ids = compiled_ids;
312 self
313 }
314
315 #[must_use]
317 pub fn with_options(mut self, options: CompileCatalogArtifactOptions<'a>) -> Self {
318 self.options = options;
319 self
320 }
321}
322
323#[derive(Debug, Clone, Copy)]
325pub enum CompileCatalogArtifactReportSelection<'a> {
326 All,
328 Selected {
330 index: &'a CompiledCatalogIdIndex,
332 compiled_ids: &'a [&'a str],
334 },
335}
336
337#[derive(Debug, Clone)]
339#[non_exhaustive]
340pub struct CompileCatalogArtifactReportOptions<'a> {
341 pub options: CompileCatalogArtifactOptions<'a>,
343 pub icu_options: CompileCatalogArtifactIcuOptions,
345 pub selection: CompileCatalogArtifactReportSelection<'a>,
347}
348
349impl<'a> CompileCatalogArtifactReportOptions<'a> {
350 #[must_use]
352 pub fn new(requested_locale: &'a str, source_locale: &'a str) -> Self {
353 Self {
354 options: CompileCatalogArtifactOptions::new(requested_locale, source_locale),
355 icu_options: CompileCatalogArtifactIcuOptions::new(),
356 selection: CompileCatalogArtifactReportSelection::All,
357 }
358 }
359
360 #[must_use]
362 pub fn selected(
363 requested_locale: &'a str,
364 source_locale: &'a str,
365 index: &'a CompiledCatalogIdIndex,
366 compiled_ids: &'a [&'a str],
367 ) -> Self {
368 Self {
369 options: CompileCatalogArtifactOptions::new(requested_locale, source_locale),
370 icu_options: CompileCatalogArtifactIcuOptions::new(),
371 selection: CompileCatalogArtifactReportSelection::Selected {
372 index,
373 compiled_ids,
374 },
375 }
376 }
377
378 #[must_use]
380 pub fn with_options(mut self, options: CompileCatalogArtifactOptions<'a>) -> Self {
381 self.options = options;
382 self
383 }
384
385 #[must_use]
387 pub fn with_icu_options(mut self, icu_options: CompileCatalogArtifactIcuOptions) -> Self {
388 self.icu_options = icu_options;
389 self
390 }
391
392 #[must_use]
394 pub fn with_selection(mut self, selection: CompileCatalogArtifactReportSelection<'a>) -> Self {
395 self.selection = selection;
396 self
397 }
398}
399
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
402#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
403#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
404pub enum CompiledCatalogTranslationKind {
405 Singular,
407 Plural,
409}
410
411#[derive(Debug, Clone, PartialEq, Eq)]
413#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
414#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
415pub struct CompiledMessage {
416 pub key: String,
418 pub source_key: CatalogMessageKey,
420 pub translation: CompiledTranslation,
422}
423
424#[derive(Debug, Clone, PartialEq, Eq, Default)]
426#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
427#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
428pub struct CompiledCatalog {
429 pub(super) entries: BTreeMap<String, CompiledMessage>,
430}
431
432impl CompiledCatalog {
433 #[must_use]
435 pub fn get(&self, key: &str) -> Option<&CompiledMessage> {
436 self.entries.get(key)
437 }
438
439 #[must_use]
441 pub fn len(&self) -> usize {
442 self.entries.len()
443 }
444
445 #[must_use]
447 pub fn is_empty(&self) -> bool {
448 self.entries.is_empty()
449 }
450
451 pub fn iter(&self) -> impl Iterator<Item = (&str, &CompiledMessage)> + '_ {
453 self.entries
454 .iter()
455 .map(|(key, message)| (key.as_str(), message))
456 }
457}
458
459#[derive(Debug, Clone, PartialEq, Eq, Default)]
461#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
462#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
463pub struct CompiledCatalogIdIndex {
464 pub(super) ids: BTreeMap<String, CatalogMessageKey>,
465}
466
467#[derive(Debug, Clone, PartialEq, Eq)]
469#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
470#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
471pub struct CompiledCatalogIdDescription {
472 pub compiled_id: String,
474 pub source_key: CatalogMessageKey,
476 pub available_locales: Vec<String>,
478 pub translation_kind: CompiledCatalogTranslationKind,
480}
481
482#[derive(Debug, Clone, PartialEq, Eq, Default)]
484#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
485#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
486pub struct DescribeCompiledIdsReport {
487 pub described: Vec<CompiledCatalogIdDescription>,
489 pub unknown_compiled_ids: Vec<String>,
491 pub unavailable_compiled_ids: Vec<CompiledCatalogUnavailableId>,
493}
494
495#[derive(Debug, Clone, PartialEq, Eq)]
497#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
498#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
499pub struct CompiledCatalogUnavailableId {
500 pub compiled_id: String,
502 pub source_key: CatalogMessageKey,
504}
505
506impl CompiledCatalogIdIndex {
507 pub fn new(
513 catalogs: &[&NormalizedParsedCatalog],
514 key_strategy: CompiledKeyStrategy,
515 ) -> Result<Self, ApiError> {
516 Self::new_with_key_generator(catalogs, key_strategy, compiled_key_for)
517 }
518
519 pub(super) fn new_with_key_generator<F>(
520 catalogs: &[&NormalizedParsedCatalog],
521 key_strategy: CompiledKeyStrategy,
522 mut key_generator: F,
523 ) -> Result<Self, ApiError>
524 where
525 F: FnMut(CompiledKeyStrategy, &CatalogMessageKey) -> String,
526 {
527 let mut ids = BTreeMap::<String, CatalogMessageKey>::new();
528
529 for catalog in catalogs {
530 for (source_key, message) in catalog.iter() {
531 if message.obsolete.is_some() {
532 continue;
533 }
534 let compiled_id = key_generator(key_strategy, source_key);
535 if let Some(existing) = ids.get(&compiled_id) {
536 if existing != source_key {
537 return Err(ApiError::Conflict(format!(
538 "compiled catalog key collision for {:?} / {:?} and {:?} / {:?} using key {}",
539 existing.msgctxt,
540 existing.msgid,
541 source_key.msgctxt,
542 source_key.msgid,
543 compiled_id
544 )));
545 }
546 continue;
547 }
548 ids.insert(compiled_id, source_key.clone());
549 }
550 }
551
552 Ok(Self { ids })
553 }
554
555 #[must_use]
557 pub fn get(&self, compiled_id: &str) -> Option<&CatalogMessageKey> {
558 self.ids.get(compiled_id)
559 }
560
561 #[must_use]
563 pub fn contains_id(&self, compiled_id: &str) -> bool {
564 self.ids.contains_key(compiled_id)
565 }
566
567 #[must_use]
569 pub fn len(&self) -> usize {
570 self.ids.len()
571 }
572
573 #[must_use]
575 pub fn is_empty(&self) -> bool {
576 self.ids.is_empty()
577 }
578
579 pub fn iter(&self) -> impl Iterator<Item = (&str, &CatalogMessageKey)> + '_ {
581 self.ids
582 .iter()
583 .map(|(compiled_id, source_key)| (compiled_id.as_str(), source_key))
584 }
585
586 #[must_use]
588 pub fn as_btreemap(&self) -> &BTreeMap<String, CatalogMessageKey> {
589 &self.ids
590 }
591
592 #[must_use]
594 pub fn into_btreemap(self) -> BTreeMap<String, CatalogMessageKey> {
595 self.ids
596 }
597
598 pub fn describe_compiled_ids(
606 &self,
607 catalogs: &[&NormalizedParsedCatalog],
608 compiled_ids: &[&str],
609 ) -> Result<DescribeCompiledIdsReport, ApiError> {
610 let locales = describe_compiled_id_catalogs(catalogs)?;
611 let mut report = DescribeCompiledIdsReport::default();
612
613 for compiled_id in std::collections::BTreeSet::from_iter(compiled_ids.iter().copied()) {
614 let Some(source_key) = self.get(compiled_id).cloned() else {
615 report.unknown_compiled_ids.push(compiled_id.to_owned());
616 continue;
617 };
618
619 let mut available_locales = Vec::new();
620 let mut translation_kind = None;
621
622 for (locale, catalog) in &locales {
623 let Some(message) = catalog.get(&source_key) else {
624 continue;
625 };
626 if message.obsolete.is_some() {
627 continue;
628 }
629 let next_kind = compiled_catalog_translation_kind_for_message(
630 catalog.parsed_catalog().semantics,
631 message,
632 );
633 if let Some(existing_kind) = translation_kind {
634 if existing_kind != next_kind {
635 return Err(ApiError::Conflict(format!(
636 "compiled ID {:?} resolves to inconsistent translation shapes across the provided catalogs",
637 compiled_id
638 )));
639 }
640 } else {
641 translation_kind = Some(next_kind);
642 }
643 available_locales.push(locale.clone());
644 }
645
646 if let Some(translation_kind) = translation_kind {
647 report.described.push(CompiledCatalogIdDescription {
648 compiled_id: compiled_id.to_owned(),
649 source_key,
650 available_locales,
651 translation_kind,
652 });
653 } else {
654 report
655 .unavailable_compiled_ids
656 .push(CompiledCatalogUnavailableId {
657 compiled_id: compiled_id.to_owned(),
658 source_key,
659 });
660 }
661 }
662
663 Ok(report)
664 }
665}
666
667#[derive(Debug, Clone, PartialEq, Eq, Default)]
674pub struct CompiledCatalogArtifact {
675 pub messages: BTreeMap<String, String>,
677 pub missing: Vec<CompiledCatalogMissingMessage>,
679 pub diagnostics: Vec<CompiledCatalogDiagnostic>,
681}
682
683#[derive(Debug, Clone, PartialEq, Eq)]
685#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
686#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
687pub struct CompiledCatalogArtifactReport {
688 pub artifact: CompiledCatalogArtifact,
691 pub provenance: CompiledCatalogProvenanceReport,
693}
694
695#[derive(Debug, Clone, PartialEq, Eq)]
697#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
698#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
699pub struct CompiledCatalogProvenanceReport {
700 pub requested_locale: String,
702 pub source_locale: String,
704 pub fallback_chain: Vec<String>,
706 pub messages: Vec<CompiledCatalogResolution>,
708}
709
710#[derive(Debug, Clone, PartialEq, Eq)]
712#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
713#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
714pub struct CompiledCatalogResolution {
715 pub key: String,
717 pub source_key: CatalogMessageKey,
719 pub resolved_locale: Option<String>,
721 pub kind: CompiledCatalogResolutionKind,
723}
724
725#[derive(Debug, Clone, Copy, PartialEq, Eq)]
727#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
728#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
729#[non_exhaustive]
730pub enum CompiledCatalogResolutionKind {
731 Requested,
733 Fallback,
735 SourceFallback,
737 Unresolved,
739}
740
741#[cfg(feature = "serde")]
742#[derive(Serialize)]
743struct CompiledCatalogArtifactWireRef<'a> {
744 schema_version: u16,
745 messages: &'a BTreeMap<String, String>,
746 missing: &'a [CompiledCatalogMissingMessage],
747 diagnostics: &'a [CompiledCatalogDiagnostic],
748}
749
750#[cfg(feature = "serde")]
751#[derive(Deserialize)]
752#[serde(deny_unknown_fields)]
753struct CompiledCatalogArtifactWire {
754 schema_version: u16,
755 #[serde(default)]
756 messages: BTreeMap<String, String>,
757 #[serde(default)]
758 missing: Vec<CompiledCatalogMissingMessage>,
759 #[serde(default)]
760 diagnostics: Vec<CompiledCatalogDiagnostic>,
761}
762
763#[cfg(feature = "serde")]
764impl Serialize for CompiledCatalogArtifact {
765 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
766 where
767 S: Serializer,
768 {
769 CompiledCatalogArtifactWireRef {
770 schema_version: COMPILED_CATALOG_ARTIFACT_SCHEMA_VERSION,
771 messages: &self.messages,
772 missing: &self.missing,
773 diagnostics: &self.diagnostics,
774 }
775 .serialize(serializer)
776 }
777}
778
779#[cfg(feature = "serde")]
780impl<'de> Deserialize<'de> for CompiledCatalogArtifact {
781 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
782 where
783 D: Deserializer<'de>,
784 {
785 let wire = CompiledCatalogArtifactWire::deserialize(deserializer)?;
786 if wire.schema_version != COMPILED_CATALOG_ARTIFACT_SCHEMA_VERSION {
787 return Err(serde::de::Error::custom(format!(
788 "unsupported compiled catalog artifact schema_version {}; expected {}",
789 wire.schema_version, COMPILED_CATALOG_ARTIFACT_SCHEMA_VERSION
790 )));
791 }
792
793 Ok(Self {
794 messages: wire.messages,
795 missing: wire.missing,
796 diagnostics: wire.diagnostics,
797 })
798 }
799}
800
801#[derive(Debug, Clone, PartialEq, Eq)]
803#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
804#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
805pub struct CompiledCatalogMissingMessage {
806 pub key: String,
808 pub source_key: CatalogMessageKey,
810 pub requested_locale: String,
812 pub resolved_locale: Option<String>,
814}
815
816#[derive(Debug, Clone, PartialEq, Eq)]
818#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
819#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
820pub struct CompiledCatalogDiagnostic {
821 pub severity: super::DiagnosticSeverity,
823 pub code: DiagnosticCode,
825 pub message: String,
827 pub key: String,
829 pub msgid: String,
831 pub msgctxt: Option<String>,
833 pub locale: String,
835}
836
837#[cfg(test)]
838mod tests {
839 use std::collections::BTreeMap;
840
841 use super::{
842 COMPILED_CATALOG_ARTIFACT_SCHEMA_VERSION, CompileCatalogArtifactIcuOptions,
843 CompileCatalogArtifactOptions, CompileCatalogArtifactReportOptions,
844 CompileCatalogArtifactReportSelection, CompileCatalogOptions,
845 CompileSelectedCatalogArtifactOptions, CompiledCatalogArtifact, CompiledCatalogDiagnostic,
846 CompiledCatalogIdIndex, CompiledCatalogMissingMessage, CompiledKeyStrategy,
847 };
848 use ferrocat_icu::{
849 IcuArgumentKind, IcuDiagnosticSeverity, IcuFormatter, IcuFormatterSupport, IcuStyleKind,
850 };
851
852 use crate::api::{CatalogMessageKey, CatalogSemantics, DiagnosticSeverity, IcuSyntaxPolicy};
853
854 #[test]
855 fn compile_option_constructors_set_required_fields_and_keep_defaults() {
856 let compile = CompileCatalogOptions::new();
857 assert_eq!(compile.key_strategy, CompiledKeyStrategy::FerrocatV1);
858 assert!(!compile.source_fallback);
859
860 let artifact = CompileCatalogArtifactOptions::new("de", "en");
861 assert_eq!(artifact.requested_locale, "de");
862 assert_eq!(artifact.source_locale, "en");
863 assert_eq!(artifact.key_strategy, CompiledKeyStrategy::FerrocatV1);
864 assert_eq!(artifact.semantics, CatalogSemantics::IcuNative);
865
866 let selected_ids = ["abc123"];
867 let selected = CompileSelectedCatalogArtifactOptions::new("de", "en", &selected_ids);
868 assert_eq!(selected.options.requested_locale, "de");
869 assert_eq!(selected.options.source_locale, "en");
870 assert_eq!(selected.compiled_ids, selected_ids.as_slice());
871 assert_eq!(
872 selected.options.key_strategy,
873 CompiledKeyStrategy::FerrocatV1
874 );
875
876 let report = CompileCatalogArtifactReportOptions::new("de", "en");
877 assert_eq!(report.options.requested_locale, "de");
878 assert_eq!(report.options.source_locale, "en");
879 assert!(matches!(
880 report.selection,
881 CompileCatalogArtifactReportSelection::All
882 ));
883 }
884
885 #[test]
886 fn compile_option_builders_set_fields() {
887 let compile = CompileCatalogOptions::new()
888 .with_key_strategy(CompiledKeyStrategy::FerrocatV1)
889 .with_source_fallback(true)
890 .with_source_locale("en")
891 .with_semantics(CatalogSemantics::GettextCompat);
892
893 assert_eq!(compile.key_strategy, CompiledKeyStrategy::FerrocatV1);
894 assert!(compile.source_fallback);
895 assert_eq!(compile.source_locale, Some("en"));
896 assert_eq!(compile.semantics, CatalogSemantics::GettextCompat);
897
898 let fallback_chain = ["fr", "en"];
899 let artifact = CompileCatalogArtifactOptions::new("de", "en")
900 .with_requested_locale("fr")
901 .with_source_locale("en-US")
902 .with_fallback_chain(&fallback_chain)
903 .with_key_strategy(CompiledKeyStrategy::FerrocatV1)
904 .with_source_fallback(true)
905 .with_strict_icu(true)
906 .with_icu_compatibility(true)
907 .with_semantics(CatalogSemantics::GettextCompat);
908
909 assert_eq!(artifact.requested_locale, "fr");
910 assert_eq!(artifact.source_locale, "en-US");
911 assert_eq!(artifact.fallback_chain, fallback_chain.as_slice());
912 assert!(artifact.source_fallback);
913 assert!(artifact.strict_icu);
914 assert!(artifact.icu_compatibility);
915 assert_eq!(artifact.semantics, CatalogSemantics::GettextCompat);
916
917 let selected_ids = ["id-1"];
918 let other_ids = ["id-2"];
919 let selected = CompileSelectedCatalogArtifactOptions::new("de", "en", &selected_ids)
920 .with_compiled_ids(&other_ids)
921 .with_options(artifact.clone());
922
923 assert_eq!(selected.compiled_ids, other_ids.as_slice());
924 assert_eq!(selected.options, artifact);
925
926 let index = CompiledCatalogIdIndex::default();
927 let selection = CompileCatalogArtifactReportSelection::Selected {
928 index: &index,
929 compiled_ids: &other_ids,
930 };
931 let report = CompileCatalogArtifactReportOptions::new("de", "en")
932 .with_options(artifact.clone())
933 .with_icu_options(
934 CompileCatalogArtifactIcuOptions::new()
935 .with_syntax_policy(IcuSyntaxPolicy::RuntimeLiteralApostrophes),
936 )
937 .with_selection(selection);
938
939 assert_eq!(report.options, artifact);
940 assert_eq!(
941 report.icu_options.syntax_policy,
942 IcuSyntaxPolicy::RuntimeLiteralApostrophes
943 );
944 assert!(matches!(
945 report.selection,
946 CompileCatalogArtifactReportSelection::Selected { compiled_ids, .. }
947 if compiled_ids == other_ids.as_slice()
948 ));
949 }
950
951 #[test]
952 fn artifact_icu_options_compare_formatter_support_callbacks_by_address() {
953 fn support_all(_: &IcuFormatter) -> IcuFormatterSupport {
954 IcuFormatterSupport::Supported
955 }
956
957 fn support_none(_: &IcuFormatter) -> IcuFormatterSupport {
958 IcuFormatterSupport::UnsupportedKind {
959 severity: IcuDiagnosticSeverity::Warning,
960 }
961 }
962
963 let left = CompileCatalogArtifactIcuOptions::new().with_formatter_support(support_all);
964 let same = CompileCatalogArtifactIcuOptions::new().with_formatter_support(support_all);
965 let different =
966 CompileCatalogArtifactIcuOptions::new().with_formatter_support(support_none);
967 let formatter = IcuFormatter {
968 name: "count".to_owned(),
969 kind: IcuArgumentKind::Number,
970 style: None,
971 style_kind: IcuStyleKind::None,
972 };
973
974 assert_eq!(left, same);
975 assert_ne!(left, different);
976 assert_ne!(left, CompileCatalogArtifactIcuOptions::new());
977 assert_eq!(support_all(&formatter), IcuFormatterSupport::Supported);
978 assert_eq!(
979 support_none(&formatter),
980 IcuFormatterSupport::UnsupportedKind {
981 severity: IcuDiagnosticSeverity::Warning
982 }
983 );
984 }
985
986 #[cfg(feature = "serde")]
987 #[test]
988 fn compiled_catalog_artifact_serde_uses_versioned_wire_contract() {
989 let artifact = CompiledCatalogArtifact {
990 messages: BTreeMap::from([("runtime-key".to_owned(), "Hallo".to_owned())]),
991 missing: vec![CompiledCatalogMissingMessage {
992 key: "runtime-key".to_owned(),
993 source_key: CatalogMessageKey::new("Hello", None),
994 requested_locale: "de".to_owned(),
995 resolved_locale: Some("en".to_owned()),
996 }],
997 diagnostics: vec![CompiledCatalogDiagnostic {
998 severity: DiagnosticSeverity::Warning,
999 code: "icu.syntax".into(),
1000 message: "invalid ICU message".to_owned(),
1001 key: "runtime-key".to_owned(),
1002 msgid: "Hello".to_owned(),
1003 msgctxt: None,
1004 locale: "de".to_owned(),
1005 }],
1006 };
1007
1008 let json = serde_json::to_value(&artifact).expect("artifact serialization must succeed");
1009 assert_eq!(
1010 json["schema_version"],
1011 COMPILED_CATALOG_ARTIFACT_SCHEMA_VERSION
1012 );
1013 assert_eq!(json["diagnostics"][0]["severity"], "warning");
1014
1015 let roundtrip: CompiledCatalogArtifact =
1016 serde_json::from_value(json).expect("artifact deserialization must succeed");
1017 assert_eq!(roundtrip, artifact);
1018 }
1019
1020 #[cfg(feature = "serde")]
1021 #[test]
1022 fn compiled_catalog_artifact_serde_rejects_unknown_schema_version() {
1023 let json = serde_json::json!({
1024 "schema_version": COMPILED_CATALOG_ARTIFACT_SCHEMA_VERSION + 1,
1025 "messages": {},
1026 "missing": [],
1027 "diagnostics": [],
1028 });
1029
1030 let error = serde_json::from_value::<CompiledCatalogArtifact>(json)
1031 .expect_err("unknown artifact schema versions must be rejected");
1032 assert!(
1033 error
1034 .to_string()
1035 .contains("unsupported compiled catalog artifact schema_version")
1036 );
1037 }
1038}