ferrocat-po 3.2.2

Performance-first PO parsing, serialization, and catalog update primitives for ferrocat.
Documentation
use super::{
    ApiError, CatalogCoverageOptions, CatalogMessageKey, CatalogMessageStatus, ParseCatalogOptions,
    PluralEncoding, measure_catalog_coverage, normalized_catalog, normalized_fcl_catalog,
    parse_catalog,
};

fn invalid_arguments_message(error: ApiError) -> String {
    match error {
        ApiError::InvalidArguments(message) => message,
        other => panic!("expected invalid arguments error, got {other:?}"),
    }
}

#[test]
fn coverage_report_counts_expected_message_statuses() {
    let source = normalized_catalog(
        concat!(
            "msgid \"Hello\"\nmsgstr \"Hello\"\n\n",
            "msgid \"Empty\"\nmsgstr \"Empty\"\n\n",
            "msgid \"Fuzzy\"\nmsgstr \"Fuzzy\"\n\n",
            "msgid \"Gone\"\nmsgstr \"Gone\"\n\n",
            "msgid \"Missing\"\nmsgstr \"Missing\"\n",
        ),
        Some("en"),
        PluralEncoding::Icu,
    );
    let target = normalized_catalog(
        concat!(
            "msgid \"Hello\"\nmsgstr \"Hallo\"\n\n",
            "msgid \"Empty\"\nmsgstr \"\"\n\n",
            "#, fuzzy\nmsgid \"Fuzzy\"\nmsgstr \"Unscharf\"\n\n",
            "#~ msgid \"Gone\"\n#~ msgstr \"Weg\"\n\n",
            "msgid \"Extra\"\nmsgstr \"Extra\"\n",
        ),
        Some("de"),
        PluralEncoding::Icu,
    );

    let report = measure_catalog_coverage(
        &[&source, &target],
        &CatalogCoverageOptions::new("en").with_details(true),
    )
    .expect("coverage");
    let locale = &report.locales[0];

    assert_eq!(report.source_messages, 5);
    assert_eq!(report.target_locales, 1);
    assert_eq!(locale.total, 5);
    assert_eq!(locale.translated, 1);
    assert_eq!(locale.empty, 1);
    assert_eq!(locale.fuzzy(), 1);
    assert_eq!(locale.obsolete, 1);
    assert_eq!(locale.missing, 1);
    assert_eq!(locale.extra, 1);
    assert_eq!(locale.incomplete(), 4);
    assert_eq!(locale.completion_percent(), 20.0);
    assert!(locale.details.iter().any(|detail| {
        detail.source_key == CatalogMessageKey::new("Fuzzy", None)
            && detail.status == CatalogMessageStatus::Fuzzy
    }));
}

#[test]
fn coverage_report_classifies_fcl_fuzzy_like_po_fuzzy() {
    let source = normalized_fcl_catalog("%FCL1\tsource=en\nFuzzy\t\tFuzzy\n", Some("en"));
    let target =
        normalized_fcl_catalog("%FCL1\tsource=en\nFuzzy\t\tUnscharf\tf=fuzzy\n", Some("de"));

    let report = measure_catalog_coverage(
        &[&source, &target],
        &CatalogCoverageOptions::new("en").with_details(true),
    )
    .expect("coverage");
    let locale = &report.locales[0];

    assert_eq!(locale.translated, 0);
    assert_eq!(locale.fuzzy(), 1);
    assert_eq!(locale.details[0].status, CatalogMessageStatus::Fuzzy);
}

#[test]
fn coverage_report_includes_optional_details() {
    let source = normalized_catalog(
        "msgid \"Hello\"\nmsgstr \"Hello\"\n",
        Some("en"),
        PluralEncoding::Icu,
    );
    let target = normalized_catalog(
        "msgid \"Hello\"\nmsgstr \"Hallo\"\n\nmsgid \"Extra\"\nmsgstr \"Extra\"\n",
        Some("de"),
        PluralEncoding::Icu,
    );

    let report = measure_catalog_coverage(
        &[&source, &target],
        &CatalogCoverageOptions::new("en").with_details(true),
    )
    .expect("coverage");
    let details = &report.locales[0].details;

    assert!(details.iter().any(|detail| {
        detail.source_key == CatalogMessageKey::new("Hello", None)
            && detail.status == CatalogMessageStatus::Translated
    }));
    assert!(details.iter().any(|detail| {
        detail.source_key == CatalogMessageKey::new("Extra", None)
            && detail.status == CatalogMessageStatus::Extra
    }));
}

#[test]
fn coverage_report_rejects_the_metadata_discarding_parse_projection() {
    let source = normalized_catalog(
        "msgid \"Hello\"\nmsgstr \"Hello\"\n",
        Some("en"),
        PluralEncoding::Icu,
    );
    let target = parse_catalog(
        ParseCatalogOptions::new(
            "#, fuzzy, x-custom\nmsgid \"Hello\"\nmsgstr \"Hallo\"\n",
            "en",
        )
        .with_locale("de"),
    )
    .expect("parse catalog")
    .into_normalized_view()
    .expect("normalize catalog");

    let error = measure_catalog_coverage(&[&source, &target], &CatalogCoverageOptions::new("en"))
        .expect_err("discarded review state must not silently count fuzzy as translated");

    assert!(
        invalid_arguments_message(error).contains("parse_catalog_for_review"),
        "error should explain the review-aware parse path"
    );
    assert!(!target.has_review_state());
}

#[test]
fn coverage_report_filters_requested_locales() {
    let source = normalized_catalog(
        "msgid \"Hello\"\nmsgstr \"Hello\"\n",
        Some("en"),
        PluralEncoding::Icu,
    );
    let de = normalized_catalog(
        "msgid \"Hello\"\nmsgstr \"Hallo\"\n",
        Some("de"),
        PluralEncoding::Icu,
    );
    let fr = normalized_catalog(
        "msgid \"Hello\"\nmsgstr \"Bonjour\"\n",
        Some("fr"),
        PluralEncoding::Icu,
    );
    let requested = ["fr"];

    let report = measure_catalog_coverage(
        &[&source, &de, &fr],
        &CatalogCoverageOptions {
            locales: &requested,
            ..CatalogCoverageOptions::new("en")
        },
    )
    .expect("coverage");

    assert_eq!(report.target_locales, 1);
    assert_eq!(report.locales[0].locale, "fr");
}

#[test]
fn coverage_report_skips_source_and_duplicate_requested_locales() {
    let source = normalized_catalog(
        "msgid \"Hello\"\nmsgstr \"Hello\"\n",
        Some("en"),
        PluralEncoding::Icu,
    );
    let de = normalized_catalog(
        "msgid \"Hello\"\nmsgstr \"Hallo\"\n",
        Some("de"),
        PluralEncoding::Icu,
    );
    let fr = normalized_catalog(
        "msgid \"Hello\"\nmsgstr \"Bonjour\"\n",
        Some("fr"),
        PluralEncoding::Icu,
    );
    let requested = ["en", "de", "de", "fr"];

    let report = measure_catalog_coverage(
        &[&source, &de, &fr],
        &CatalogCoverageOptions {
            locales: &requested,
            ..CatalogCoverageOptions::new("en")
        },
    )
    .expect("coverage");

    assert_eq!(report.target_locales, 2);
    assert_eq!(report.locales[0].locale, "de");
    assert_eq!(report.locales[1].locale, "fr");
}

#[test]
fn coverage_report_treats_empty_source_set_as_complete() {
    let source = normalized_catalog("", Some("en"), PluralEncoding::Icu);
    let target = normalized_catalog("", Some("de"), PluralEncoding::Icu);

    let report = measure_catalog_coverage(&[&source, &target], &CatalogCoverageOptions::new("en"))
        .expect("coverage");
    let locale = &report.locales[0];

    assert_eq!(report.source_messages, 0);
    assert_eq!(locale.total, 0);
    assert_eq!(locale.incomplete(), 0);
    assert_eq!(locale.completion_ratio(), 1.0);
    assert_eq!(locale.completion_percent(), 100.0);
    assert!(locale.details.is_empty());
}

#[test]
fn coverage_report_classifies_plural_messages_with_empty_slots() {
    let source = normalized_catalog(
        concat!(
            "msgid \"book\"\n",
            "msgid_plural \"books\"\n",
            "msgstr[0] \"book\"\n",
            "msgstr[1] \"books\"\n",
        ),
        Some("en"),
        PluralEncoding::Gettext,
    );
    let target = normalized_catalog(
        concat!(
            "msgid \"book\"\n",
            "msgid_plural \"books\"\n",
            "msgstr[0] \"Buch\"\n",
            "msgstr[1] \"\"\n",
        ),
        Some("de"),
        PluralEncoding::Gettext,
    );

    let report = measure_catalog_coverage(
        &[&source, &target],
        &CatalogCoverageOptions::new("en").with_details(true),
    )
    .expect("coverage");
    let locale = &report.locales[0];

    assert_eq!(locale.empty, 1);
    assert!(locale.details.iter().any(|detail| {
        detail.source_key == CatalogMessageKey::new("book", None)
            && detail.status == CatalogMessageStatus::Empty
    }));
}

#[test]
fn coverage_statuses_are_mutually_exclusive_when_fuzzy_overlaps_other_state() {
    let source = normalized_catalog(
        concat!(
            "msgid \"partial\"\n",
            "msgid_plural \"partials\"\n",
            "msgstr[0] \"partial\"\n",
            "msgstr[1] \"partials\"\n\n",
            "msgid \"obsolete\"\nmsgstr \"obsolete\"\n",
        ),
        Some("en"),
        PluralEncoding::Gettext,
    );
    let target = normalized_catalog(
        concat!(
            "#, fuzzy\n",
            "msgid \"partial\"\n",
            "msgid_plural \"partials\"\n",
            "msgstr[0] \"Teil\"\n",
            "msgstr[1] \"\"\n\n",
            "#~ #, fuzzy\n",
            "#~ msgid \"obsolete\"\n",
            "#~ msgstr \"veraltet\"\n\n",
            "#, fuzzy\n",
            "msgid \"extra\"\n",
            "msgstr \"zusätzlich\"\n",
        ),
        Some("de"),
        PluralEncoding::Gettext,
    );

    let report = measure_catalog_coverage(
        &[&source, &target],
        &CatalogCoverageOptions::new("en").with_details(true),
    )
    .expect("coverage");
    let locale = &report.locales[0];

    assert_eq!(locale.translated, 0);
    assert_eq!(locale.fuzzy(), 0);
    assert_eq!(locale.empty, 1);
    assert_eq!(locale.obsolete, 1);
    assert_eq!(locale.missing, 0);
    assert_eq!(locale.extra, 1);
    assert_eq!(
        locale.total,
        locale.translated + locale.fuzzy() + locale.empty + locale.obsolete + locale.missing
    );
}

#[test]
fn coverage_report_rejects_invalid_catalog_locale_inputs() {
    let source = normalized_catalog(
        "msgid \"Hello\"\nmsgstr \"Hello\"\n",
        Some("en"),
        PluralEncoding::Icu,
    );
    let duplicate_source = normalized_catalog(
        "msgid \"Bye\"\nmsgstr \"Bye\"\n",
        Some("en"),
        PluralEncoding::Icu,
    );
    let missing_locale = normalized_catalog(
        "msgid \"Hello\"\nmsgstr \"Hallo\"\n",
        None,
        PluralEncoding::Icu,
    );
    let de = normalized_catalog(
        "msgid \"Hello\"\nmsgstr \"Hallo\"\n",
        Some("de"),
        PluralEncoding::Icu,
    );
    let requested = ["fr"];

    let missing_source = measure_catalog_coverage(&[&de], &CatalogCoverageOptions::new("en"))
        .expect_err("missing source locale should fail");
    assert!(invalid_arguments_message(missing_source).contains("source locale"));

    let duplicate_locale = measure_catalog_coverage(
        &[&source, &duplicate_source],
        &CatalogCoverageOptions::new("en"),
    )
    .expect_err("duplicate locale should fail");
    assert!(invalid_arguments_message(duplicate_locale).contains("duplicate catalog locale"));

    let undeclared_locale =
        measure_catalog_coverage(&[&missing_locale], &CatalogCoverageOptions::new("en"))
            .expect_err("missing declared locale should fail");
    assert!(invalid_arguments_message(undeclared_locale).contains("declare a locale"));

    let missing_requested = measure_catalog_coverage(
        &[&source, &de],
        &CatalogCoverageOptions {
            locales: &requested,
            ..CatalogCoverageOptions::new("en")
        },
    )
    .expect_err("missing requested locale should fail");
    assert!(invalid_arguments_message(missing_requested).contains("requested locale"));
}