#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de};
use std::collections::HashMap;
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct MultilingualConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub title_mode: Option<MultilingualMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name_mode: Option<MultilingualMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preferred_script: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preferred_transliteration: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub scripts: HashMap<String, ScriptConfig>,
#[serde(
default,
rename = "realization-default",
skip_serializing_if = "RealizationDefault::is_latin"
)]
pub realization_default: RealizationDefault,
#[serde(skip_serializing_if = "Option::is_none")]
pub punctuation_width: Option<PunctuationWidth>,
#[serde(
default,
rename = "term-locale",
skip_serializing_if = "TermLocale::is_style"
)]
pub term_locale: TermLocale,
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum TermLocale {
#[default]
Style,
Item,
}
impl TermLocale {
pub fn is_style(&self) -> bool {
matches!(self, TermLocale::Style)
}
}
#[derive(Debug, PartialEq, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum MultilingualMode {
Primary,
Transliterated,
Translated,
Combined,
Pattern(Vec<MultilingualSegment>),
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct MultilingualSegment {
pub view: MultilingualView,
#[serde(default, skip_serializing_if = "SegmentWrap::is_none")]
pub wrap: SegmentWrap,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum MultilingualView {
OriginalScript,
Transliterated,
Translated,
}
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum SegmentWrap {
#[default]
None,
Brackets,
Parentheses,
}
impl SegmentWrap {
pub fn is_none(&self) -> bool {
matches!(self, SegmentWrap::None)
}
pub fn apply(&self, text: &str) -> String {
match self {
SegmentWrap::None => text.to_string(),
SegmentWrap::Brackets => format!("[{text}]"),
SegmentWrap::Parentheses => format!("({text})"),
}
}
}
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct ScriptConfig {
#[serde(default)]
pub use_native_ordering: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub delimiter: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_separator: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub punctuation: Option<PunctuationStyle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub realization: Option<PunctuationRealization>,
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct PunctuationRealization {
#[serde(skip_serializing_if = "Option::is_none")]
pub comma: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub colon: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub semicolon: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub period: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parentheses: Option<[String; 2]>,
#[serde(skip_serializing_if = "Option::is_none")]
pub brackets: Option<[String; 2]>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum PunctuationStyle {
Latin,
FullWidth,
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum RealizationDefault {
#[default]
Latin,
Cjk,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum PunctuationWidth {
Half,
Full,
Mixed,
Bylan,
}
impl RealizationDefault {
pub fn is_latin(&self) -> bool {
matches!(self, RealizationDefault::Latin)
}
}
impl<'de> de::Deserialize<'de> for MultilingualMode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
struct ModeVisitor;
impl<'de> de::Visitor<'de> for ModeVisitor {
type Value = MultilingualMode;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"a multilingual mode string (\"primary\", \"transliterated\", \
\"translated\", \"combined\") or a pattern object {{pattern: [...]}}"
)
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
match v {
"primary" => Ok(MultilingualMode::Primary),
"transliterated" => Ok(MultilingualMode::Transliterated),
"translated" => Ok(MultilingualMode::Translated),
"combined" => Ok(MultilingualMode::Combined),
_ => Err(E::unknown_variant(
v,
&[
"primary",
"transliterated",
"translated",
"combined",
"pattern",
],
)),
}
}
fn visit_map<A: de::MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let key: String = map
.next_key()?
.ok_or_else(|| de::Error::custom("expected \"pattern\" key, got empty map"))?;
if key != "pattern" {
return Err(de::Error::unknown_field(&key, &["pattern"]));
}
let segments: Vec<MultilingualSegment> = map.next_value()?;
if map.next_key::<String>()?.is_some() {
return Err(de::Error::custom("unexpected extra key in pattern object"));
}
Ok(MultilingualMode::Pattern(segments))
}
fn visit_enum<A: de::EnumAccess<'de>>(self, data: A) -> Result<Self::Value, A::Error> {
use de::VariantAccess as _;
let (variant, access): (String, _) = data.variant()?;
if variant == "pattern" {
let segments: Vec<MultilingualSegment> = access.newtype_variant()?;
Ok(MultilingualMode::Pattern(segments))
} else {
Err(de::Error::unknown_variant(
&variant,
&[
"primary",
"transliterated",
"translated",
"combined",
"pattern",
],
))
}
}
}
deserializer.deserialize_any(ModeVisitor)
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::todo,
clippy::unimplemented,
clippy::unreachable,
clippy::get_unwrap,
reason = "Panicking is acceptable and often desired in tests."
)]
mod term_locale_tests {
use super::*;
#[test]
fn term_locale_item_round_trips_through_yaml() {
let config: MultilingualConfig = serde_yaml::from_str("term-locale: item").unwrap();
assert_eq!(config.term_locale, TermLocale::Item);
let yaml = serde_yaml::to_string(&config).unwrap();
assert!(yaml.contains("term-locale: item"));
let round_tripped: MultilingualConfig = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(round_tripped, config);
}
#[test]
fn absent_term_locale_field_defaults_to_style() {
let config: MultilingualConfig = serde_yaml::from_str("{}").unwrap();
assert_eq!(config.term_locale, TermLocale::Style);
}
#[test]
fn default_term_locale_is_omitted_on_serialize() {
let config = MultilingualConfig::default();
let yaml = serde_yaml::to_string(&config).unwrap();
assert!(
!yaml.contains("term-locale"),
"term-locale: style is today's byte-identical default and must be omitted: {yaml}"
);
}
}