csaf-crud 1.4.17

CSAF 2.0 / 2.1 advisory CRUD server with HATEOAS JSON API and HTML UI (TLS 1.3, HTTP/1.1 + HTTP/2 + HTTP/3)
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Pierre Gronau, ndaal in Cologne

//! The [`Lang`] enum — one variant per supported UI language.
//!
//! `En`/`De`/`Fr`/`Es`/`It` are pinned first (this product's historical
//! order — do not reorder them); the remaining 44 sort alphabetically by
//! **English name** (endonyms across Latin/Cyrillic/Greek/Georgian/Armenian/
//! Han/Devanagari/Perso-Arabic scripts do not collate — never sort by
//! endonym; see `skills/languages-europe-rust` §1). The variant order IS the
//! menu order and must stay in lock-step with [`Lang::all`] and the embedded
//! TSV table in [`super`].
//!
//! The three Asian languages (Chinese `Zh`, Hindi `Hi`, Urdu `Ur`) are added
//! per `skills/languages-asia`: `Zh` between Catalan and Croatian, `Hi`
//! between Greek and Hungarian, `Ur` last. Urdu is right-to-left — see
//! [`Lang::dir`].

/// A supported user-interface language.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Lang {
    /// English (the default / fallback).
    #[default]
    En,
    /// German (Deutsch).
    De,
    /// French (Français).
    Fr,
    /// Spanish (Español).
    Es,
    /// Italian (Italiano).
    It,
    /// Albanian (Shqip).
    Sq,
    /// Armenian (Հայերեն).
    Hy,
    /// Azerbaijani (Azərbaycanca).
    Az,
    /// Belarusian (Беларуская).
    Be,
    /// Bosnian (Bosanski).
    Bs,
    /// Bulgarian (Български).
    Bg,
    /// Catalan (Català).
    Ca,
    /// Chinese, Mandarin (中文).
    Zh,
    /// Croatian (Hrvatski).
    Hr,
    /// Czech (Čeština).
    Cs,
    /// Danish (Dansk).
    Da,
    /// Dutch (Nederlands).
    Nl,
    /// Estonian (Eesti).
    Et,
    /// Finnish (Suomi).
    Fi,
    /// Georgian (ქართული).
    Ka,
    /// Greek (Ελληνικά).
    El,
    /// Hindi (हिन्दी).
    Hi,
    /// Hungarian (Magyar).
    Hu,
    /// Icelandic (Íslenska).
    Is,
    /// Irish (Gaeilge).
    Ga,
    /// Latin (Latina).
    La,
    /// Latvian (Latviešu).
    Lv,
    /// Lithuanian (Lietuvių).
    Lt,
    /// Lule Sámi (Julevsámegiella).
    Smj,
    /// Luxembourgish (Lëtzebuergesch).
    Lb,
    /// Macedonian (Македонски).
    Mk,
    /// Maltese (Malti).
    Mt,
    /// Montenegrin (Crnogorski).
    Cnr,
    /// Northern Sámi (Davvisámegiella).
    Se,
    /// Norwegian Bokmål (Norsk bokmål).
    No,
    /// Norwegian Nynorsk (Norsk nynorsk).
    Nn,
    /// Polish (Polski).
    Pl,
    /// Portuguese (Português).
    Pt,
    /// Romanian (Română).
    Ro,
    /// Romansh (Rumantsch).
    Rm,
    /// Russian (Русский).
    Ru,
    /// Serbian (Српски).
    Sr,
    /// Slovak (Slovenčina).
    Sk,
    /// Slovenian (Slovenščina).
    Sl,
    /// Southern Sámi (Åarjelsaemien gïele).
    Sma,
    /// Swedish (Svenska).
    Sv,
    /// Turkish (Türkçe).
    Tr,
    /// Ukrainian (Українська).
    Uk,
    /// Urdu (اردو).
    Ur,
}

impl Lang {
    /// The BCP-47 primary subtag (ISO 639-1 where one exists, else ISO 639-3
    /// — `cnr` Montenegrin, `smj`/`sma` Lule/Southern Sámi) — also the
    /// `<html lang>` value.
    #[must_use]
    pub const fn code(self) -> &'static str {
        match self {
            Self::En => "en",
            Self::De => "de",
            Self::Fr => "fr",
            Self::Es => "es",
            Self::It => "it",
            Self::Sq => "sq",
            Self::Hy => "hy",
            Self::Az => "az",
            Self::Be => "be",
            Self::Bs => "bs",
            Self::Bg => "bg",
            Self::Ca => "ca",
            Self::Zh => "zh",
            Self::Hr => "hr",
            Self::Cs => "cs",
            Self::Da => "da",
            Self::Nl => "nl",
            Self::Et => "et",
            Self::Fi => "fi",
            Self::Ka => "ka",
            Self::El => "el",
            Self::Hi => "hi",
            Self::Hu => "hu",
            Self::Is => "is",
            Self::Ga => "ga",
            Self::La => "la",
            Self::Lv => "lv",
            Self::Lt => "lt",
            Self::Smj => "smj",
            Self::Lb => "lb",
            Self::Mk => "mk",
            Self::Mt => "mt",
            Self::Cnr => "cnr",
            Self::Se => "se",
            Self::No => "no",
            Self::Nn => "nn",
            Self::Pl => "pl",
            Self::Pt => "pt",
            Self::Ro => "ro",
            Self::Rm => "rm",
            Self::Ru => "ru",
            Self::Sr => "sr",
            Self::Sk => "sk",
            Self::Sl => "sl",
            Self::Sma => "sma",
            Self::Sv => "sv",
            Self::Tr => "tr",
            Self::Uk => "uk",
            Self::Ur => "ur",
        }
    }

    /// The language's own name (endonym), for the switcher menu.
    #[must_use]
    pub const fn endonym(self) -> &'static str {
        match self {
            Self::En => "English",
            Self::De => "Deutsch",
            Self::Fr => "Français",
            Self::Es => "Español",
            Self::It => "Italiano",
            Self::Sq => "Shqip",
            Self::Hy => "Հայերեն",
            Self::Az => "Azərbaycanca",
            Self::Be => "Беларуская",
            Self::Bs => "Bosanski",
            Self::Bg => "Български",
            Self::Ca => "Català",
            Self::Zh => "中文",
            Self::Hr => "Hrvatski",
            Self::Cs => "Čeština",
            Self::Da => "Dansk",
            Self::Nl => "Nederlands",
            Self::Et => "Eesti",
            Self::Fi => "Suomi",
            Self::Ka => "ქართული",
            Self::El => "Ελληνικά",
            Self::Hi => "हिन्दी",
            Self::Hu => "Magyar",
            Self::Is => "Íslenska",
            Self::Ga => "Gaeilge",
            Self::La => "Latina",
            Self::Lv => "Latviešu",
            Self::Lt => "Lietuvių",
            Self::Smj => "Julevsámegiella",
            Self::Lb => "Lëtzebuergesch",
            Self::Mk => "Македонски",
            Self::Mt => "Malti",
            Self::Cnr => "Crnogorski",
            Self::Se => "Davvisámegiella",
            Self::No => "Norsk bokmål",
            Self::Nn => "Norsk nynorsk",
            Self::Pl => "Polski",
            Self::Pt => "Português",
            Self::Ro => "Română",
            Self::Rm => "Rumantsch",
            Self::Ru => "Русский",
            Self::Sr => "Српски",
            Self::Sk => "Slovenčina",
            Self::Sl => "Slovenščina",
            Self::Sma => "Åarjelsaemien gïele",
            Self::Sv => "Svenska",
            Self::Tr => "Türkçe",
            Self::Uk => "Українська",
            Self::Ur => "اردو",
        }
    }

    /// Parse a language code (case-insensitively, primary subtag only), or
    /// `None` for an unsupported language.
    #[must_use]
    pub fn from_code(code: &str) -> Option<Self> {
        // Take the primary subtag (`de-DE` -> `de`) and lower-case it.
        let primary = code.split(['-', '_']).next().unwrap_or(code);
        match primary.to_ascii_lowercase().as_str() {
            "en" => Some(Self::En),
            "de" => Some(Self::De),
            "fr" => Some(Self::Fr),
            "es" => Some(Self::Es),
            "it" => Some(Self::It),
            "sq" => Some(Self::Sq),
            "hy" => Some(Self::Hy),
            "az" => Some(Self::Az),
            "be" => Some(Self::Be),
            "bs" => Some(Self::Bs),
            "bg" => Some(Self::Bg),
            "ca" => Some(Self::Ca),
            // The primary-subtag split reduces `zh-CN` / `zh-Hans` / `zh-TW`
            // / `zh-Hant` to `zh`, so Chinese needs no alias table (contrast
            // the deliberate `"no" | "nb"` for Norwegian Bokmål). Traditional
            // variants resolve to this Simplified entry — a documented limit.
            "zh" => Some(Self::Zh),
            "hr" => Some(Self::Hr),
            "cs" => Some(Self::Cs),
            "da" => Some(Self::Da),
            "nl" => Some(Self::Nl),
            "et" => Some(Self::Et),
            "fi" => Some(Self::Fi),
            "ka" => Some(Self::Ka),
            "el" => Some(Self::El),
            "hi" => Some(Self::Hi),
            "hu" => Some(Self::Hu),
            "is" => Some(Self::Is),
            "ga" => Some(Self::Ga),
            "la" => Some(Self::La),
            "lv" => Some(Self::Lv),
            "lt" => Some(Self::Lt),
            "smj" => Some(Self::Smj),
            "lb" => Some(Self::Lb),
            "mk" => Some(Self::Mk),
            "mt" => Some(Self::Mt),
            "cnr" => Some(Self::Cnr),
            "se" => Some(Self::Se),
            // `nb` is the ISO 639-1 code browsers send for Bokmål-configured
            // Norwegian; the UI entry keeps the macrolanguage code `no`.
            "no" | "nb" => Some(Self::No),
            "nn" => Some(Self::Nn),
            "pl" => Some(Self::Pl),
            "pt" => Some(Self::Pt),
            "ro" => Some(Self::Ro),
            "rm" => Some(Self::Rm),
            "ru" => Some(Self::Ru),
            "sr" => Some(Self::Sr),
            "sk" => Some(Self::Sk),
            "sl" => Some(Self::Sl),
            "sma" => Some(Self::Sma),
            "sv" => Some(Self::Sv),
            "tr" => Some(Self::Tr),
            "uk" => Some(Self::Uk),
            "ur" => Some(Self::Ur),
            _ => None,
        }
    }

    /// The text direction for `<html dir>`. Urdu (Perso-Arabic script) is
    /// right-to-left; every other supported language is left-to-right.
    ///
    /// Deliberately a wildcard match, not an exhaustive one: direction is a
    /// binary property with a single RTL member today. The trade-off is that
    /// the compiler will NOT force a future RTL language (Arabic, Hebrew,
    /// Persian) through this method — a new variant would silently get
    /// `"ltr"`. The `urdu_is_rtl_and_every_other_language_is_ltr` test
    /// iterates [`Lang::all`], so a mis-wired new language fails a test even
    /// though it compiles; make this match exhaustive when a second RTL
    /// language lands.
    #[must_use]
    pub const fn dir(self) -> &'static str {
        match self {
            Self::Ur => "rtl",
            _ => "ltr",
        }
    }

    /// All supported languages, in switcher-menu order.
    #[must_use]
    pub const fn all() -> [Self; 49] {
        [
            Self::En,
            Self::De,
            Self::Fr,
            Self::Es,
            Self::It,
            Self::Sq,
            Self::Hy,
            Self::Az,
            Self::Be,
            Self::Bs,
            Self::Bg,
            Self::Ca,
            Self::Zh,
            Self::Hr,
            Self::Cs,
            Self::Da,
            Self::Nl,
            Self::Et,
            Self::Fi,
            Self::Ka,
            Self::El,
            Self::Hi,
            Self::Hu,
            Self::Is,
            Self::Ga,
            Self::La,
            Self::Lv,
            Self::Lt,
            Self::Smj,
            Self::Lb,
            Self::Mk,
            Self::Mt,
            Self::Cnr,
            Self::Se,
            Self::No,
            Self::Nn,
            Self::Pl,
            Self::Pt,
            Self::Ro,
            Self::Rm,
            Self::Ru,
            Self::Sr,
            Self::Sk,
            Self::Sl,
            Self::Sma,
            Self::Sv,
            Self::Tr,
            Self::Uk,
            Self::Ur,
        ]
    }
}