use std::collections::BTreeMap;
use super::{
ApiError, CatalogMessageKey, CatalogSemantics, NormalizedParsedCatalog,
compile::{
compiled_catalog_translation_kind_for_message, compiled_key_for,
describe_compiled_id_catalogs,
},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompiledTranslation {
Singular(String),
Plural(BTreeMap<String, String>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CompiledKeyStrategy {
#[default]
FerrocatV1,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompileCatalogOptions<'a> {
pub key_strategy: CompiledKeyStrategy,
pub source_fallback: bool,
pub source_locale: Option<&'a str>,
pub semantics: CatalogSemantics,
}
impl Default for CompileCatalogOptions<'_> {
fn default() -> Self {
Self {
key_strategy: CompiledKeyStrategy::FerrocatV1,
source_fallback: false,
source_locale: None,
semantics: CatalogSemantics::IcuNative,
}
}
}
impl CompileCatalogOptions<'_> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompileCatalogArtifactOptions<'a> {
pub requested_locale: &'a str,
pub source_locale: &'a str,
pub fallback_chain: &'a [String],
pub key_strategy: CompiledKeyStrategy,
pub source_fallback: bool,
pub strict_icu: bool,
pub semantics: CatalogSemantics,
}
impl Default for CompileCatalogArtifactOptions<'_> {
fn default() -> Self {
Self {
requested_locale: "",
source_locale: "",
fallback_chain: &[],
key_strategy: CompiledKeyStrategy::FerrocatV1,
source_fallback: false,
strict_icu: false,
semantics: CatalogSemantics::IcuNative,
}
}
}
impl<'a> CompileCatalogArtifactOptions<'a> {
#[must_use]
pub fn new(requested_locale: &'a str, source_locale: &'a str) -> Self {
Self {
requested_locale,
source_locale,
..Self::default()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompileSelectedCatalogArtifactOptions<'a> {
pub requested_locale: &'a str,
pub source_locale: &'a str,
pub fallback_chain: &'a [String],
pub key_strategy: CompiledKeyStrategy,
pub source_fallback: bool,
pub strict_icu: bool,
pub semantics: CatalogSemantics,
pub compiled_ids: &'a [String],
}
impl Default for CompileSelectedCatalogArtifactOptions<'_> {
fn default() -> Self {
Self {
requested_locale: "",
source_locale: "",
fallback_chain: &[],
key_strategy: CompiledKeyStrategy::FerrocatV1,
source_fallback: false,
strict_icu: false,
semantics: CatalogSemantics::IcuNative,
compiled_ids: &[],
}
}
}
impl<'a> CompileSelectedCatalogArtifactOptions<'a> {
#[must_use]
pub fn new(
requested_locale: &'a str,
source_locale: &'a str,
compiled_ids: &'a [String],
) -> Self {
Self {
requested_locale,
source_locale,
compiled_ids,
..Self::default()
}
}
pub(super) fn artifact_options(&self) -> CompileCatalogArtifactOptions<'_> {
CompileCatalogArtifactOptions {
requested_locale: self.requested_locale,
source_locale: self.source_locale,
fallback_chain: self.fallback_chain,
key_strategy: self.key_strategy,
source_fallback: self.source_fallback,
strict_icu: self.strict_icu,
semantics: self.semantics,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompiledCatalogTranslationKind {
Singular,
Plural,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompiledMessage {
pub key: String,
pub source_key: CatalogMessageKey,
pub translation: CompiledTranslation,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CompiledCatalog {
pub(super) entries: BTreeMap<String, CompiledMessage>,
}
impl CompiledCatalog {
#[must_use]
pub fn get(&self, key: &str) -> Option<&CompiledMessage> {
self.entries.get(key)
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &CompiledMessage)> + '_ {
self.entries
.iter()
.map(|(key, message)| (key.as_str(), message))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CompiledCatalogIdIndex {
pub(super) ids: BTreeMap<String, CatalogMessageKey>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompiledCatalogIdDescription {
pub compiled_id: String,
pub source_key: CatalogMessageKey,
pub available_locales: Vec<String>,
pub translation_kind: CompiledCatalogTranslationKind,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct DescribeCompiledIdsReport {
pub described: Vec<CompiledCatalogIdDescription>,
pub unknown_compiled_ids: Vec<String>,
pub unavailable_compiled_ids: Vec<CompiledCatalogUnavailableId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompiledCatalogUnavailableId {
pub compiled_id: String,
pub source_key: CatalogMessageKey,
}
impl CompiledCatalogIdIndex {
pub fn new(
catalogs: &[&NormalizedParsedCatalog],
key_strategy: CompiledKeyStrategy,
) -> Result<Self, ApiError> {
Self::new_with_key_generator(catalogs, key_strategy, compiled_key_for)
}
pub(super) fn new_with_key_generator<F>(
catalogs: &[&NormalizedParsedCatalog],
key_strategy: CompiledKeyStrategy,
mut key_generator: F,
) -> Result<Self, ApiError>
where
F: FnMut(CompiledKeyStrategy, &CatalogMessageKey) -> String,
{
let mut ids = BTreeMap::<String, CatalogMessageKey>::new();
for catalog in catalogs {
for (source_key, message) in catalog.iter() {
if message.obsolete {
continue;
}
let compiled_id = key_generator(key_strategy, source_key);
if let Some(existing) = ids.get(&compiled_id) {
if existing != source_key {
return Err(ApiError::Conflict(format!(
"compiled catalog key collision for {:?} / {:?} and {:?} / {:?} using key {}",
existing.msgctxt,
existing.msgid,
source_key.msgctxt,
source_key.msgid,
compiled_id
)));
}
continue;
}
ids.insert(compiled_id, source_key.clone());
}
}
Ok(Self { ids })
}
#[must_use]
pub fn get(&self, compiled_id: &str) -> Option<&CatalogMessageKey> {
self.ids.get(compiled_id)
}
#[must_use]
pub fn contains_id(&self, compiled_id: &str) -> bool {
self.ids.contains_key(compiled_id)
}
#[must_use]
pub fn len(&self) -> usize {
self.ids.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.ids.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &CatalogMessageKey)> + '_ {
self.ids
.iter()
.map(|(compiled_id, source_key)| (compiled_id.as_str(), source_key))
}
#[must_use]
pub fn as_btreemap(&self) -> &BTreeMap<String, CatalogMessageKey> {
&self.ids
}
#[must_use]
pub fn into_btreemap(self) -> BTreeMap<String, CatalogMessageKey> {
self.ids
}
pub fn describe_compiled_ids(
&self,
catalogs: &[&NormalizedParsedCatalog],
compiled_ids: &[String],
) -> Result<DescribeCompiledIdsReport, ApiError> {
let locales = describe_compiled_id_catalogs(catalogs)?;
let mut report = DescribeCompiledIdsReport::default();
for compiled_id in std::collections::BTreeSet::from_iter(compiled_ids.iter().cloned()) {
let Some(source_key) = self.get(&compiled_id).cloned() else {
report.unknown_compiled_ids.push(compiled_id);
continue;
};
let mut available_locales = Vec::new();
let mut translation_kind = None;
for (locale, catalog) in &locales {
let Some(message) = catalog.get(&source_key) else {
continue;
};
if message.obsolete {
continue;
}
let next_kind = compiled_catalog_translation_kind_for_message(
catalog.parsed_catalog().semantics,
message,
);
if let Some(existing_kind) = translation_kind {
if existing_kind != next_kind {
return Err(ApiError::Conflict(format!(
"compiled ID {:?} resolves to inconsistent translation shapes across the provided catalogs",
compiled_id
)));
}
} else {
translation_kind = Some(next_kind);
}
available_locales.push(locale.clone());
}
if let Some(translation_kind) = translation_kind {
report.described.push(CompiledCatalogIdDescription {
compiled_id,
source_key,
available_locales,
translation_kind,
});
} else {
report
.unavailable_compiled_ids
.push(CompiledCatalogUnavailableId {
compiled_id,
source_key,
});
}
}
Ok(report)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CompiledCatalogArtifact {
pub messages: BTreeMap<String, String>,
pub missing: Vec<CompiledCatalogMissingMessage>,
pub diagnostics: Vec<CompiledCatalogDiagnostic>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompiledCatalogMissingMessage {
pub key: String,
pub source_key: CatalogMessageKey,
pub requested_locale: String,
pub resolved_locale: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompiledCatalogDiagnostic {
pub severity: super::DiagnosticSeverity,
pub code: String,
pub message: String,
pub key: String,
pub msgid: String,
pub msgctxt: Option<String>,
pub locale: String,
}
#[cfg(test)]
mod tests {
use super::{
CompileCatalogArtifactOptions, CompileCatalogOptions,
CompileSelectedCatalogArtifactOptions, CompiledKeyStrategy,
};
use crate::api::CatalogSemantics;
#[test]
fn compile_option_constructors_set_required_fields_and_keep_defaults() {
let compile = CompileCatalogOptions::new();
assert_eq!(compile.key_strategy, CompiledKeyStrategy::FerrocatV1);
assert!(!compile.source_fallback);
let artifact = CompileCatalogArtifactOptions::new("de", "en");
assert_eq!(artifact.requested_locale, "de");
assert_eq!(artifact.source_locale, "en");
assert_eq!(artifact.key_strategy, CompiledKeyStrategy::FerrocatV1);
assert_eq!(artifact.semantics, CatalogSemantics::IcuNative);
let selected_ids = vec!["abc123".to_owned()];
let selected = CompileSelectedCatalogArtifactOptions::new("de", "en", &selected_ids);
assert_eq!(selected.requested_locale, "de");
assert_eq!(selected.source_locale, "en");
assert_eq!(selected.compiled_ids, selected_ids.as_slice());
assert_eq!(selected.key_strategy, CompiledKeyStrategy::FerrocatV1);
}
}