1use std::collections::BTreeMap;
2
3use super::{
4 ApiError, CatalogMessageKey, CatalogSemantics, NormalizedParsedCatalog,
5 compile::{
6 compiled_catalog_translation_kind_for_message, compiled_key_for,
7 describe_compiled_id_catalogs,
8 },
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum CompiledTranslation {
14 Singular(String),
16 Plural(BTreeMap<String, String>),
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum CompiledKeyStrategy {
23 #[default]
27 FerrocatV1,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct CompileCatalogOptions<'a> {
33 pub key_strategy: CompiledKeyStrategy,
35 pub source_fallback: bool,
37 pub source_locale: Option<&'a str>,
39 pub semantics: CatalogSemantics,
41}
42
43impl Default for CompileCatalogOptions<'_> {
44 fn default() -> Self {
45 Self {
46 key_strategy: CompiledKeyStrategy::FerrocatV1,
47 source_fallback: false,
48 source_locale: None,
49 semantics: CatalogSemantics::IcuNative,
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct CompileCatalogArtifactOptions<'a> {
57 pub requested_locale: &'a str,
59 pub source_locale: &'a str,
61 pub fallback_chain: &'a [String],
63 pub key_strategy: CompiledKeyStrategy,
65 pub source_fallback: bool,
67 pub strict_icu: bool,
69 pub semantics: CatalogSemantics,
71}
72
73impl Default for CompileCatalogArtifactOptions<'_> {
74 fn default() -> Self {
75 Self {
76 requested_locale: "",
77 source_locale: "",
78 fallback_chain: &[],
79 key_strategy: CompiledKeyStrategy::FerrocatV1,
80 source_fallback: false,
81 strict_icu: false,
82 semantics: CatalogSemantics::IcuNative,
83 }
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct CompileSelectedCatalogArtifactOptions<'a> {
90 pub requested_locale: &'a str,
92 pub source_locale: &'a str,
94 pub fallback_chain: &'a [String],
96 pub key_strategy: CompiledKeyStrategy,
98 pub source_fallback: bool,
100 pub strict_icu: bool,
102 pub semantics: CatalogSemantics,
104 pub compiled_ids: &'a [String],
106}
107
108impl Default for CompileSelectedCatalogArtifactOptions<'_> {
109 fn default() -> Self {
110 Self {
111 requested_locale: "",
112 source_locale: "",
113 fallback_chain: &[],
114 key_strategy: CompiledKeyStrategy::FerrocatV1,
115 source_fallback: false,
116 strict_icu: false,
117 semantics: CatalogSemantics::IcuNative,
118 compiled_ids: &[],
119 }
120 }
121}
122
123impl CompileSelectedCatalogArtifactOptions<'_> {
124 pub(super) fn artifact_options(&self) -> CompileCatalogArtifactOptions<'_> {
125 CompileCatalogArtifactOptions {
126 requested_locale: self.requested_locale,
127 source_locale: self.source_locale,
128 fallback_chain: self.fallback_chain,
129 key_strategy: self.key_strategy,
130 source_fallback: self.source_fallback,
131 strict_icu: self.strict_icu,
132 semantics: self.semantics,
133 }
134 }
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum CompiledCatalogTranslationKind {
140 Singular,
142 Plural,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct CompiledMessage {
149 pub key: String,
151 pub source_key: CatalogMessageKey,
153 pub translation: CompiledTranslation,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Default)]
159pub struct CompiledCatalog {
160 pub(super) entries: BTreeMap<String, CompiledMessage>,
161}
162
163impl CompiledCatalog {
164 #[must_use]
166 pub fn get(&self, key: &str) -> Option<&CompiledMessage> {
167 self.entries.get(key)
168 }
169
170 #[must_use]
172 pub fn len(&self) -> usize {
173 self.entries.len()
174 }
175
176 #[must_use]
178 pub fn is_empty(&self) -> bool {
179 self.entries.is_empty()
180 }
181
182 pub fn iter(&self) -> impl Iterator<Item = (&str, &CompiledMessage)> + '_ {
184 self.entries
185 .iter()
186 .map(|(key, message)| (key.as_str(), message))
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Default)]
192pub struct CompiledCatalogIdIndex {
193 pub(super) ids: BTreeMap<String, CatalogMessageKey>,
194}
195
196#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct CompiledCatalogIdDescription {
199 pub compiled_id: String,
201 pub source_key: CatalogMessageKey,
203 pub available_locales: Vec<String>,
205 pub translation_kind: CompiledCatalogTranslationKind,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq, Default)]
211pub struct DescribeCompiledIdsReport {
212 pub described: Vec<CompiledCatalogIdDescription>,
214 pub unknown_compiled_ids: Vec<String>,
216 pub unavailable_compiled_ids: Vec<CompiledCatalogUnavailableId>,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq)]
222pub struct CompiledCatalogUnavailableId {
223 pub compiled_id: String,
225 pub source_key: CatalogMessageKey,
227}
228
229impl CompiledCatalogIdIndex {
230 pub fn new(
236 catalogs: &[&NormalizedParsedCatalog],
237 key_strategy: CompiledKeyStrategy,
238 ) -> Result<Self, ApiError> {
239 Self::new_with_key_generator(catalogs, key_strategy, compiled_key_for)
240 }
241
242 pub(super) fn new_with_key_generator<F>(
243 catalogs: &[&NormalizedParsedCatalog],
244 key_strategy: CompiledKeyStrategy,
245 mut key_generator: F,
246 ) -> Result<Self, ApiError>
247 where
248 F: FnMut(CompiledKeyStrategy, &CatalogMessageKey) -> String,
249 {
250 let mut ids = BTreeMap::<String, CatalogMessageKey>::new();
251
252 for catalog in catalogs {
253 for (source_key, message) in catalog.iter() {
254 if message.obsolete {
255 continue;
256 }
257 let compiled_id = key_generator(key_strategy, source_key);
258 if let Some(existing) = ids.get(&compiled_id) {
259 if existing != source_key {
260 return Err(ApiError::Conflict(format!(
261 "compiled catalog key collision for {:?} / {:?} and {:?} / {:?} using key {}",
262 existing.msgctxt,
263 existing.msgid,
264 source_key.msgctxt,
265 source_key.msgid,
266 compiled_id
267 )));
268 }
269 continue;
270 }
271 ids.insert(compiled_id, source_key.clone());
272 }
273 }
274
275 Ok(Self { ids })
276 }
277
278 #[must_use]
280 pub fn get(&self, compiled_id: &str) -> Option<&CatalogMessageKey> {
281 self.ids.get(compiled_id)
282 }
283
284 #[must_use]
286 pub fn contains_id(&self, compiled_id: &str) -> bool {
287 self.ids.contains_key(compiled_id)
288 }
289
290 #[must_use]
292 pub fn len(&self) -> usize {
293 self.ids.len()
294 }
295
296 #[must_use]
298 pub fn is_empty(&self) -> bool {
299 self.ids.is_empty()
300 }
301
302 pub fn iter(&self) -> impl Iterator<Item = (&str, &CatalogMessageKey)> + '_ {
304 self.ids
305 .iter()
306 .map(|(compiled_id, source_key)| (compiled_id.as_str(), source_key))
307 }
308
309 #[must_use]
311 pub fn as_btreemap(&self) -> &BTreeMap<String, CatalogMessageKey> {
312 &self.ids
313 }
314
315 #[must_use]
317 pub fn into_btreemap(self) -> BTreeMap<String, CatalogMessageKey> {
318 self.ids
319 }
320
321 pub fn describe_compiled_ids(
329 &self,
330 catalogs: &[&NormalizedParsedCatalog],
331 compiled_ids: &[String],
332 ) -> Result<DescribeCompiledIdsReport, ApiError> {
333 let locales = describe_compiled_id_catalogs(catalogs)?;
334 let mut report = DescribeCompiledIdsReport::default();
335
336 for compiled_id in std::collections::BTreeSet::from_iter(compiled_ids.iter().cloned()) {
337 let Some(source_key) = self.get(&compiled_id).cloned() else {
338 report.unknown_compiled_ids.push(compiled_id);
339 continue;
340 };
341
342 let mut available_locales = Vec::new();
343 let mut translation_kind = None;
344
345 for (locale, catalog) in &locales {
346 let Some(message) = catalog.get(&source_key) else {
347 continue;
348 };
349 if message.obsolete {
350 continue;
351 }
352 let next_kind = compiled_catalog_translation_kind_for_message(
353 catalog.parsed_catalog().semantics,
354 message,
355 );
356 if let Some(existing_kind) = translation_kind {
357 if existing_kind != next_kind {
358 return Err(ApiError::Conflict(format!(
359 "compiled ID {:?} resolves to inconsistent translation shapes across the provided catalogs",
360 compiled_id
361 )));
362 }
363 } else {
364 translation_kind = Some(next_kind);
365 }
366 available_locales.push(locale.clone());
367 }
368
369 if let Some(translation_kind) = translation_kind {
370 report.described.push(CompiledCatalogIdDescription {
371 compiled_id,
372 source_key,
373 available_locales,
374 translation_kind,
375 });
376 } else {
377 report
378 .unavailable_compiled_ids
379 .push(CompiledCatalogUnavailableId {
380 compiled_id,
381 source_key,
382 });
383 }
384 }
385
386 Ok(report)
387 }
388}
389
390#[derive(Debug, Clone, PartialEq, Eq, Default)]
392pub struct CompiledCatalogArtifact {
393 pub messages: BTreeMap<String, String>,
395 pub missing: Vec<CompiledCatalogMissingMessage>,
397 pub diagnostics: Vec<CompiledCatalogDiagnostic>,
399}
400
401#[derive(Debug, Clone, PartialEq, Eq)]
403pub struct CompiledCatalogMissingMessage {
404 pub key: String,
406 pub source_key: CatalogMessageKey,
408 pub requested_locale: String,
410 pub resolved_locale: Option<String>,
412}
413
414#[derive(Debug, Clone, PartialEq, Eq)]
416pub struct CompiledCatalogDiagnostic {
417 pub severity: super::DiagnosticSeverity,
419 pub code: String,
421 pub message: String,
423 pub key: String,
425 pub msgid: String,
427 pub msgctxt: Option<String>,
429 pub locale: String,
431}