diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8c5d7..2ca78d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,40 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.ht --- +## [2.13.0] — 2026-07-07 + +Neues Qualitäts-Scoring nach der EU-MQA-Methodik (Phase 1). Siehe `docs/MQA-KONZEPT.md`. + +### ✨ Changed — Qualitätsprüfung auf MQA umgestellt +- Die Qualitätsprüfung folgt jetzt dem **Metadata Quality Assessment (MQA)** von data.europa.eu: + **5 FAIR-Dimensionen** (Auffindbarkeit, Zugänglichkeit, Interoperabilität, Wiederverwendbarkeit, + Kontext), **405 Punkte**, **4 Bewertungsstufen** (Ausgezeichnet / Gut / Ausreichend / Mangelhaft). +- Die Metriken stehen in der neuen Single-Source-of-Truth `config/mqa-metrics.php`. +- Die Meta-Box zeigt die Bewertung pro Dimension inkl. Punkte und Metrik-Status. +- Das JSON-LD/REST-Feld `odw:qualityScore` weist nun MQA-Punkte (von 405), die Bewertungsstufe und + die Dimensions-Aufschlüsselung aus (verweist auf die MQA-Methodik-URL). + +### 🔧 Phasenumfang +- **Phase 1 (dieses Release):** Alle „gesetzt?"-Metriken werden bewertet (offline). +- Vokabular-, URL-Erreichbarkeits- und DCAT-AP-SHACL-Metriken sind als **„nicht bewertet"** verdrahtet + und werden aus dem bewertbaren Maximum herausgerechnet; die Bewertungsstufen werden **proportional** + auf das bewertbare Maximum skaliert. Diese Metriken folgen in Phase 2 (Vokabular) und Phase 3 + (Netzwerk/SHACL). + +### ♻️ Abwärtskompatibilität +- Die Publish-Validierung (`config/dcat-ap-fields.php`) bleibt unverändert. +- Der abgeleitete 0–100-Score und das Level bleiben in `_odw_quality_score` / `_odw_quality_level` + erhalten (Admin-Listenspalte, Sortierung). Das vollständige MQA-Ergebnis liegt in `_odw_mqa`. + +### 🌍 i18n +- 33 neue MQA-UI-Strings in `.po`/`.pot` ergänzt und `en_US.mo` neu kompiliert. + +### ✅ Tests +- Neue Tests für Bewertungsstufen (proportionale Schwellen), Metrik-Summen je Dimension (405 gesamt) + und die MQA-Persistierung/JSON-LD-Ausgabe. + +--- + ## [2.12.0] — 2026-06-28 UX-Feinschliff und Vervollständigung der optionalen DCAT-AP-3.0-Felder. diff --git a/README.md b/README.md index 10ab821..b160337 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ![PHP Version](https://img.shields.io/badge/PHP-%3E%3D%208.1-8892BF?style=flat-square&logo=php&logoColor=white) ![WordPress](https://img.shields.io/badge/WordPress-compatible-21759B?style=flat-square&logo=wordpress&logoColor=white) ![DCAT-AP](https://img.shields.io/badge/DCAT--AP-3.0-brightgreen?style=flat-square) -![Version](https://img.shields.io/badge/Version-2.12.0-brightgreen?style=flat-square) +![Version](https://img.shields.io/badge/Version-2.13.0-brightgreen?style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-willkommen-brightgreen?style=flat-square) 📖 [Dokumentation](DOCUMENTATION.md) · 📐 [Technische Spezifikation](TECHNICAL-SPEC.md) · 📝 [Changelog](CHANGELOG.md) · 🛡️ [Security](SECURITY.md) · ⚖️ [Lizenz](LICENSE) diff --git a/assets/css/admin.css b/assets/css/admin.css index 6d69d32..54f2d00 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -344,6 +344,23 @@ .odw-quality-fail .odw-quality-col-status { color: var(--odw-color-quality-low-dot); } .odw-quality-fail { color: var(--odw-color-text-muted); } +/* MQA: not-assessed metrics (network / vocabulary / SHACL — pending phases) */ +.odw-quality-notassessed { color: var(--odw-color-text-muted); } +.odw-quality-notassessed .odw-quality-col-status { color: var(--odw-color-border); } + +/* MQA: per-dimension score in the section header */ +.odw-quality-dim-score { + float: right; + font-weight: 400; + color: var(--odw-color-text-muted); +} + +.odw-quality-of-max { + font-weight: 400; + color: var(--odw-color-text-muted); + font-size: 12px; +} + /* Footer */ .odw-quality-footer { margin-top: 12px; diff --git a/config/mqa-metrics.php b/config/mqa-metrics.php new file mode 100644 index 0000000..eace9bd --- /dev/null +++ b/config/mqa-metrics.php @@ -0,0 +1,261 @@ + 'keyword', + 'dimension' => 'findability', + 'dcat_prop' => 'dcat:keyword', + 'label' => __( 'Schlüsselwörter (dcat:keyword)', 'open-data-wizard' ), + 'points' => 30, + 'type' => 'present', + 'check' => 'keyword', + ), + array( + 'key' => 'theme', + 'dimension' => 'findability', + 'dcat_prop' => 'dcat:theme', + 'label' => __( 'Kategorien (dcat:theme)', 'open-data-wizard' ), + 'points' => 30, + 'type' => 'present', + 'check' => 'theme', + ), + array( + 'key' => 'spatial', + 'dimension' => 'findability', + 'dcat_prop' => 'dct:spatial', + 'label' => __( 'Räumliche Abdeckung (dct:spatial)', 'open-data-wizard' ), + 'points' => 20, + 'type' => 'present', + 'check' => 'spatial', + ), + array( + 'key' => 'temporal', + 'dimension' => 'findability', + 'dcat_prop' => 'dct:temporal', + 'label' => __( 'Zeitliche Abdeckung (dct:temporal)', 'open-data-wizard' ), + 'points' => 20, + 'type' => 'present', + 'check' => 'temporal', + ), + + // ------------------------------------------------------------------------- + // Zugänglichkeit (max. 100) + // ------------------------------------------------------------------------- + array( + 'key' => 'access_url_reachable', + 'dimension' => 'accessibility', + 'dcat_prop' => 'dcat:accessURL', + 'label' => __( 'AccessURL erreichbar (dcat:accessURL)', 'open-data-wizard' ), + 'points' => 50, + 'type' => 'reachable', + 'check' => 'access_url', + ), + array( + 'key' => 'download_url', + 'dimension' => 'accessibility', + 'dcat_prop' => 'dcat:downloadURL', + 'label' => __( 'DownloadURL gesetzt (dcat:downloadURL)', 'open-data-wizard' ), + 'points' => 20, + 'type' => 'present', + 'check' => 'download_url', + ), + array( + 'key' => 'download_url_reachable', + 'dimension' => 'accessibility', + 'dcat_prop' => 'dcat:downloadURL', + 'label' => __( 'DownloadURL erreichbar (dcat:downloadURL)', 'open-data-wizard' ), + 'points' => 30, + 'type' => 'reachable', + 'check' => 'download_url', + ), + + // ------------------------------------------------------------------------- + // Interoperabilität (max. 110) + // ------------------------------------------------------------------------- + array( + 'key' => 'format', + 'dimension' => 'interoperability', + 'dcat_prop' => 'dct:format', + 'label' => __( 'Format gesetzt (dct:format)', 'open-data-wizard' ), + 'points' => 20, + 'type' => 'present', + 'check' => 'format', + ), + array( + 'key' => 'media_type', + 'dimension' => 'interoperability', + 'dcat_prop' => 'dcat:mediaType', + 'label' => __( 'Media-Type gesetzt (dcat:mediaType)', 'open-data-wizard' ), + 'points' => 10, + 'type' => 'present', + 'check' => 'media_type', + ), + array( + 'key' => 'format_vocab', + 'dimension' => 'interoperability', + 'dcat_prop' => 'dct:format', + 'label' => __( 'Format/Media-Type aus Vokabular', 'open-data-wizard' ), + 'points' => 10, + 'type' => 'vocab', + 'check' => 'format_vocab', + ), + array( + 'key' => 'format_nonproprietary', + 'dimension' => 'interoperability', + 'dcat_prop' => 'dct:format', + 'label' => __( 'Nicht-proprietäres Format', 'open-data-wizard' ), + 'points' => 20, + 'type' => 'vocab', + 'check' => 'format_nonproprietary', + ), + array( + 'key' => 'format_machine_readable', + 'dimension' => 'interoperability', + 'dcat_prop' => 'dct:format', + 'label' => __( 'Maschinenlesbares Format', 'open-data-wizard' ), + 'points' => 20, + 'type' => 'vocab', + 'check' => 'format_machine_readable', + ), + array( + 'key' => 'dcat_ap_compliance', + 'dimension' => 'interoperability', + 'dcat_prop' => 'dcat-ap', + 'label' => __( 'DCAT-AP-Konformität (SHACL)', 'open-data-wizard' ), + 'points' => 30, + 'type' => 'shacl', + 'check' => 'shacl', + ), + + // ------------------------------------------------------------------------- + // Wiederverwendbarkeit (max. 75) + // ------------------------------------------------------------------------- + array( + 'key' => 'license', + 'dimension' => 'reusability', + 'dcat_prop' => 'dct:license', + 'label' => __( 'Lizenz gesetzt (dct:license)', 'open-data-wizard' ), + 'points' => 20, + 'type' => 'present', + 'check' => 'license', + ), + array( + 'key' => 'license_vocab', + 'dimension' => 'reusability', + 'dcat_prop' => 'dct:license', + 'label' => __( 'Lizenz aus Vokabular', 'open-data-wizard' ), + 'points' => 10, + 'type' => 'vocab', + 'check' => 'license_vocab', + ), + array( + 'key' => 'access_rights', + 'dimension' => 'reusability', + 'dcat_prop' => 'dct:accessRights', + 'label' => __( 'Zugriffsrechte gesetzt (dct:accessRights)', 'open-data-wizard' ), + 'points' => 10, + 'type' => 'present', + 'check' => 'access_rights', + ), + array( + 'key' => 'access_rights_vocab', + 'dimension' => 'reusability', + 'dcat_prop' => 'dct:accessRights', + 'label' => __( 'Zugriffsrechte aus Vokabular', 'open-data-wizard' ), + 'points' => 5, + 'type' => 'vocab', + 'check' => 'access_rights_vocab', + ), + array( + 'key' => 'contact_point', + 'dimension' => 'reusability', + 'dcat_prop' => 'dcat:contactPoint', + 'label' => __( 'Kontaktpunkt (dcat:contactPoint)', 'open-data-wizard' ), + 'points' => 20, + 'type' => 'present', + 'check' => 'contact_point', + ), + array( + 'key' => 'publisher', + 'dimension' => 'reusability', + 'dcat_prop' => 'dct:publisher', + 'label' => __( 'Herausgeber (dct:publisher)', 'open-data-wizard' ), + 'points' => 10, + 'type' => 'present', + 'check' => 'publisher', + ), + + // ------------------------------------------------------------------------- + // Kontext (max. 20) + // ------------------------------------------------------------------------- + array( + 'key' => 'rights', + 'dimension' => 'contextuality', + 'dcat_prop' => 'dct:rights', + 'label' => __( 'Rechte (dct:rights)', 'open-data-wizard' ), + 'points' => 5, + 'type' => 'present', + 'check' => 'rights', + ), + array( + 'key' => 'byte_size', + 'dimension' => 'contextuality', + 'dcat_prop' => 'dcat:byteSize', + 'label' => __( 'Dateigröße (dcat:byteSize)', 'open-data-wizard' ), + 'points' => 5, + 'type' => 'present', + 'check' => 'byte_size', + ), + array( + 'key' => 'issued', + 'dimension' => 'contextuality', + 'dcat_prop' => 'dct:issued', + 'label' => __( 'Ausstellungsdatum (dct:issued)', 'open-data-wizard' ), + 'points' => 5, + 'type' => 'present', + 'check' => 'issued', + ), + array( + 'key' => 'modified', + 'dimension' => 'contextuality', + 'dcat_prop' => 'dct:modified', + 'label' => __( 'Änderungsdatum (dct:modified)', 'open-data-wizard' ), + 'points' => 5, + 'type' => 'present', + 'check' => 'modified', + ), +); +// Summe: 100 + 100 + 110 + 75 + 20 = 405. diff --git a/config/phpstan.neon b/config/phpstan.neon index 1836c80..5d15608 100644 --- a/config/phpstan.neon +++ b/config/phpstan.neon @@ -55,5 +55,3 @@ parameters: # PHPDoc type strictness causes false positives with WordPress types - identifier: function.impossibleType message: '/Call to function is_wp_error.*will always evaluate/' - - - identifier: identical.alwaysTrue diff --git a/includes/class-admin.php b/includes/class-admin.php index 0f502d0..42f9cd3 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -153,7 +153,8 @@ public static function render_column( string $column, int $post_id ): void { } else { $level = $quality['level']; $score = $quality['score']; - $label = ODW_Quality::get_level_label( $level ); + $rating = (string) ( $quality['rating'] ?? '' ); + $label = '' !== $rating ? ODW_Quality::get_rating_label( $rating ) : ODW_Quality::get_level_label( $level ); $title_attr = sprintf( '%s · %d/100 %s', $label, $score, __( 'Punkte', 'open-data-wizard' ) ); printf( ' %d', diff --git a/includes/class-quality.php b/includes/class-quality.php index 2dac924..a6aafdc 100644 --- a/includes/class-quality.php +++ b/includes/class-quality.php @@ -1,15 +1,15 @@ + * @return array */ - public static function get_indicators(): array { - if ( class_exists( 'ODW_Fields' ) ) { - $defs = ODW_Fields::load_field_definitions(); - if ( ! empty( $defs ) ) { - return $defs; - } + public static function get_metrics(): array { + $file = ODW_PLUGIN_DIR . 'config/mqa-metrics.php'; + + if ( ! file_exists( $file ) ) { + return array(); } - // Fallback when ODW_Fields is not available (e.g. unit tests). + $data = include $file; + return is_array( $data ) ? $data : array(); + } + + /** + * Menschenlesbare Labels der Dimensionen. + * + * @return array + */ + public static function get_dimension_labels(): array { return array( - array( - 'key' => 'title', - 'label' => __( 'Titel (dct:title)', 'open-data-wizard' ), - 'points' => 10, - 'required' => true, - ), - array( - 'key' => 'description', - 'label' => __( 'Beschreibung (dct:description)', 'open-data-wizard' ), - 'points' => 10, - 'required' => true, - ), - array( - 'key' => 'publisher', - 'label' => __( 'Herausgeber (dct:publisher)', 'open-data-wizard' ), - 'points' => 10, - 'required' => true, - ), - array( - 'key' => 'license', - 'label' => __( 'Lizenz (dct:license)', 'open-data-wizard' ), - 'points' => 10, - 'required' => true, - ), - array( - 'key' => 'distribution', - 'label' => __( 'Distribution mit URL (dcat:accessURL)', 'open-data-wizard' ), - 'points' => 15, - 'required' => true, - ), - array( - 'key' => 'language', - 'label' => __( 'Sprache (dct:language)', 'open-data-wizard' ), - 'points' => 10, - 'required' => false, - ), - array( - 'key' => 'keywords', - 'label' => __( 'Schlagworte (dcat:keyword)', 'open-data-wizard' ), - 'points' => 10, - 'required' => false, - ), - array( - 'key' => 'theme', - 'label' => __( 'Thema (dcat:theme)', 'open-data-wizard' ), - 'points' => 10, - 'required' => false, - ), - array( - 'key' => 'issued', - 'label' => __( 'Veröffentlichungsdatum (dct:issued)', 'open-data-wizard' ), - 'points' => 10, - 'required' => false, - ), - array( - 'key' => 'dist_format', - 'label' => __( 'Format der Distribution (dct:format)', 'open-data-wizard' ), - 'points' => 5, - 'required' => false, - ), + 'findability' => __( 'Auffindbarkeit', 'open-data-wizard' ), + 'accessibility' => __( 'Zugänglichkeit', 'open-data-wizard' ), + 'interoperability' => __( 'Interoperabilität', 'open-data-wizard' ), + 'reusability' => __( 'Wiederverwendbarkeit', 'open-data-wizard' ), + 'contextuality' => __( 'Kontext', 'open-data-wizard' ), ); } @@ -142,10 +108,10 @@ public static function get_indicators(): array { // ------------------------------------------------------------------------- /** - * Berechnet den Qualitätsscore für einen Datensatz. + * Berechnet den MQA-Qualitätsscore für einen Datensatz. * * @param int $post_id Dataset post ID. - * @return array{score: int, level: string, indicators: array, calculated_at: string} + * @return array MQA-Ergebnis inkl. abgeleitetem 0–100-Score und Legacy-Level. */ public static function calculate( int $post_id ): array { $post = get_post( $post_id ); @@ -154,133 +120,200 @@ public static function calculate( int $post_id ): array { return self::empty_result(); } - $total = 0; - $breakdown = array(); + $dimensions = array(); + foreach ( self::DIMENSIONS as $dim ) { + $dimensions[ $dim ] = array( + 'achieved' => 0, + 'assessable' => 0, + 'max' => 0, + ); + } - foreach ( self::get_indicators() as $indicator ) { - $passed = self::check_indicator( $indicator['key'], $post ); - $earned = $passed ? $indicator['points'] : 0; - $total += $earned; - - $breakdown[ $indicator['key'] ] = array( - 'label' => $indicator['label'], - 'points' => $indicator['points'], - 'earned' => $earned, - 'passed' => $passed, - 'required' => $indicator['required'], + $metrics = array(); + + foreach ( self::get_metrics() as $metric ) { + $dim = $metric['dimension']; + if ( ! isset( $dimensions[ $dim ] ) ) { + $dimensions[ $dim ] = array( + 'achieved' => 0, + 'assessable' => 0, + 'max' => 0, + ); + } + + $points = (int) $metric['points']; + $dimensions[ $dim ]['max'] += $points; + + $passed = self::evaluate_metric( $metric, $post ); + + if ( null === $passed ) { + $status = 'not_assessed'; + } else { + $dimensions[ $dim ]['assessable'] += $points; + if ( $passed ) { + $dimensions[ $dim ]['achieved'] += $points; + } + $status = $passed ? 'passed' : 'failed'; + } + + $metrics[ $metric['key'] ] = array( + 'label' => $metric['label'], + 'dimension' => $dim, + 'points' => $points, + 'status' => $status, ); } - // Clamp into the documented 0–100 range so a changed config can never - // produce an out-of-range score. - $total = max( 0, min( 100, $total ) ); + $achieved = (int) array_sum( array_column( $dimensions, 'achieved' ) ); + $assessable = (int) array_sum( array_column( $dimensions, 'assessable' ) ); + $rating = self::get_rating( $achieved, $assessable ); return array( - 'score' => $total, - 'level' => self::get_level( $total ), - 'indicators' => $breakdown, + 'achieved' => $achieved, + 'assessable' => $assessable, + 'max' => self::MQA_MAX, + 'rating' => $rating, + 'dimensions' => $dimensions, + 'metrics' => $metrics, 'calculated_at' => current_time( 'Y-m-d H:i:s' ), + // Abwärtskompatibilität für Admin-Spalte, Sortierung und Alt-Konsumenten. + 'score' => $assessable > 0 ? (int) round( $achieved / $assessable * 100 ) : 0, + 'level' => self::rating_to_level( $rating ), ); } /** - * Sum of points for all required indicators — the score reached when exactly - * the required fields are filled. Computed from the active config so the level - * thresholds stay correct if the point values change. + * Wertet eine einzelne Metrik aus. * - * @return int Required-only score threshold. + * @param array $metric Metrik-Definition. + * @param \WP_Post $post Dataset post object. + * @return bool|null true = erfüllt, false = nicht erfüllt, null = nicht bewertet (Vokabular/Netzwerk/SHACL). */ - private static function get_required_only_score(): int { - $sum = 0; - foreach ( self::get_indicators() as $indicator ) { - if ( ! empty( $indicator['required'] ) ) { - $sum += (int) $indicator['points']; - } + private static function evaluate_metric( array $metric, \WP_Post $post ): ?bool { + if ( 'present' !== ( $metric['type'] ?? '' ) ) { + // Vokabular-, Erreichbarkeits- und SHACL-Prüfungen folgen in späteren Phasen. + return null; } - $threshold = $sum > 0 ? $sum : self::REQUIRED_ONLY_SCORE; - - // Keep the threshold within the clamped 0–100 score range so get_level() - // stays consistent even if a filter pushes the required points above 100. - return min( 100, $threshold ); + return self::check_metric( (string) $metric['check'], $post ); } /** - * Prüft einen einzelnen Indikator am WP_Post-Objekt. + * „Ist die Eigenschaft gesetzt?"-Prüfung je Metrik. * - * @param string $key Indicator key (e.g. 'title', 'license'). - * @param \WP_Post $post Dataset post object. - * @return bool True when the indicator passes. + * @param string $check Check-Schlüssel aus der Metrik-Definition. + * @param \WP_Post $post Dataset post object. + * @return bool True wenn die Eigenschaft im Datensatz vorhanden ist. */ - private static function check_indicator( string $key, \WP_Post $post ): bool { - switch ( $key ) { - case 'title': - return '' !== trim( $post->post_title ); + private static function check_metric( string $check, \WP_Post $post ): bool { + $id = $post->ID; + + switch ( $check ) { + case 'keyword': + $raw = (string) carbon_get_post_meta( $id, 'odw_keywords' ); + $parts = preg_split( '/\r?\n/', $raw ); + $keywords = is_array( $parts ) ? array_filter( array_map( 'trim', $parts ) ) : array(); + return ! empty( $keywords ); - case 'description': - return '' !== trim( (string) carbon_get_post_meta( $post->ID, 'odw_description' ) ); + case 'theme': + return '' !== trim( (string) carbon_get_post_meta( $id, 'odw_theme' ) ); - case 'publisher': - return '' !== trim( (string) carbon_get_post_meta( $post->ID, 'odw_publisher' ) ); + case 'spatial': + return '' !== trim( (string) carbon_get_post_meta( $id, 'odw_spatial' ) ); + + case 'temporal': + return '' !== trim( (string) carbon_get_post_meta( $id, 'odw_temporal_start' ) ) + || '' !== trim( (string) carbon_get_post_meta( $id, 'odw_temporal_end' ) ); + + case 'download_url': + return '' !== trim( (string) carbon_get_post_meta( $id, 'odw_download_url' ) ); + + case 'format': + return '' !== trim( (string) carbon_get_post_meta( $id, 'odw_format' ) ); + + case 'media_type': + return '' !== trim( (string) carbon_get_post_meta( $id, 'odw_media_type' ) ); case 'license': - $lic = (string) carbon_get_post_meta( $post->ID, 'odw_license' ); + $lic = (string) carbon_get_post_meta( $id, 'odw_license' ); if ( '' !== $lic && 'sonstige' !== $lic ) { return true; } if ( 'sonstige' === $lic ) { - return '' !== (string) carbon_get_post_meta( $post->ID, 'odw_license_custom' ); + return '' !== trim( (string) carbon_get_post_meta( $id, 'odw_license_custom' ) ); } return false; - case 'distribution': - return '' !== (string) carbon_get_post_meta( $post->ID, 'odw_access_url' ); + case 'access_rights': + return '' !== trim( (string) carbon_get_post_meta( $id, 'odw_access_rights' ) ); - case 'language': - return '' !== trim( (string) carbon_get_post_meta( $post->ID, 'odw_language' ) ); + case 'contact_point': + return '' !== trim( (string) carbon_get_post_meta( $id, 'odw_contact_name' ) ) + || '' !== trim( (string) carbon_get_post_meta( $id, 'odw_contact_email' ) ) + || '' !== trim( (string) carbon_get_post_meta( $id, 'odw_contact_url' ) ); - case 'keywords': - $raw = (string) carbon_get_post_meta( $post->ID, 'odw_keywords' ); - $keywords = array_filter( array_map( 'trim', preg_split( '/\r?\n/', $raw ) ) ); - return ! empty( $keywords ); + case 'publisher': + return '' !== trim( (string) carbon_get_post_meta( $id, 'odw_publisher' ) ); - case 'theme': - return '' !== trim( (string) carbon_get_post_meta( $post->ID, 'odw_theme' ) ); + case 'rights': + return '' !== trim( (string) carbon_get_post_meta( $id, 'odw_dist_rights' ) ); + + case 'byte_size': + return (int) carbon_get_post_meta( $id, 'odw_byte_size' ) > 0; case 'issued': - return '' !== trim( (string) carbon_get_post_meta( $post->ID, 'odw_issued' ) ); + return '' !== trim( (string) carbon_get_post_meta( $id, 'odw_issued' ) ); - case 'dist_format': - return '' !== (string) carbon_get_post_meta( $post->ID, 'odw_format' ); + case 'modified': + return '' !== trim( (string) carbon_get_post_meta( $id, 'odw_modified' ) ) + || '' !== trim( (string) get_post_meta( $id, '_odw_modified', true ) ); } return false; } /** - * Ermittelt das Qualitätslevel aus dem Score. - * - * 100 → Perfekt (alle Felder ausgefüllt) - * 56–99 → Gut (alle Pflichtfelder + einige optionale) - * REQUIRED_ONLY → Ausreichend (genau alle Pflichtfelder, keine optionalen) - * < REQUIRED → Verbesserungsbedarf (Pflichtfelder fehlen) + * Ermittelt die MQA-Bewertungsstufe aus erreichten/bewertbaren Punkten. + * Die MQA-Schwellen (351/221/121 von 405) werden proportional auf das + * bewertbare Maximum skaliert. * - * @param int $score Numeric score 0–100. - * @return string One of LEVEL_PERFECT, LEVEL_HIGH, LEVEL_SUFFICIENT, LEVEL_LOW. + * @param int $achieved Erreichte Punkte. + * @param int $assessable Bewertbares Maximum. + * @return string Eine der RATING_* Konstanten. */ - public static function get_level( int $score ): string { - $required_only = self::get_required_only_score(); + public static function get_rating( int $achieved, int $assessable ): string { + if ( $assessable <= 0 ) { + return self::RATING_BAD; + } - if ( 100 === $score ) { - return self::LEVEL_PERFECT; + $ratio = $achieved / $assessable; + + if ( $ratio >= self::RATING_EXCELLENT_RATIO ) { + return self::RATING_EXCELLENT; } - if ( $score > $required_only ) { - return self::LEVEL_HIGH; + if ( $ratio >= self::RATING_GOOD_RATIO ) { + return self::RATING_GOOD; } - if ( $required_only === $score ) { - return self::LEVEL_SUFFICIENT; + if ( $ratio >= self::RATING_SUFFICIENT_RATIO ) { + return self::RATING_SUFFICIENT; } - return self::LEVEL_LOW; + return self::RATING_BAD; + } + + /** + * Bildet eine MQA-Bewertungsstufe auf eine Legacy-Level-Konstante ab + * (für Admin-Spalte und Badge-CSS). + * + * @param string $rating Eine der RATING_* Konstanten. + * @return string Eine der LEVEL_* Konstanten. + */ + private static function rating_to_level( string $rating ): string { + return array( + self::RATING_EXCELLENT => self::LEVEL_PERFECT, + self::RATING_GOOD => self::LEVEL_HIGH, + self::RATING_SUFFICIENT => self::LEVEL_SUFFICIENT, + self::RATING_BAD => self::LEVEL_LOW, + )[ $rating ] ?? self::LEVEL_LOW; } // ------------------------------------------------------------------------- @@ -291,23 +324,16 @@ public static function get_level( int $score ): string { * Holt gespeicherte Qualitätsdaten aus Post-Meta. * * @param int $post_id Dataset post ID. - * @return array{score: int, level: string, indicators: array, calculated_at: string} + * @return array */ public static function get( int $post_id ): array { - $level = (string) get_post_meta( $post_id, '_odw_quality_level', true ); + $stored = get_post_meta( $post_id, '_odw_mqa', true ); - if ( '' === $level ) { + if ( ! is_array( $stored ) || empty( $stored['rating'] ) ) { return self::empty_result(); } - $indicators = get_post_meta( $post_id, '_odw_quality_indicators', true ); - - return array( - 'score' => (int) get_post_meta( $post_id, '_odw_quality_score', true ), - 'level' => $level, - 'indicators' => is_array( $indicators ) ? $indicators : array(), - 'calculated_at' => (string) get_post_meta( $post_id, '_odw_quality_calculated_at', true ), - ); + return $stored; } /** @@ -317,10 +343,12 @@ public static function get( int $post_id ): array { * @param array $result Result array from calculate(). */ public static function store( int $post_id, array $result ): void { - update_post_meta( $post_id, '_odw_quality_score', $result['score'] ); - update_post_meta( $post_id, '_odw_quality_level', $result['level'] ); - update_post_meta( $post_id, '_odw_quality_indicators', $result['indicators'] ); - update_post_meta( $post_id, '_odw_quality_calculated_at', $result['calculated_at'] ); + update_post_meta( $post_id, '_odw_mqa', $result ); + + // Abwärtskompatible Skalar-Meta für Admin-Spalte + Sortierung. + update_post_meta( $post_id, '_odw_quality_score', $result['score'] ?? 0 ); + update_post_meta( $post_id, '_odw_quality_level', $result['level'] ?? '' ); + update_post_meta( $post_id, '_odw_quality_calculated_at', $result['calculated_at'] ?? '' ); } /** @@ -337,13 +365,11 @@ public static function recalculate_on_save( int $post_id ): void { return; } - // Now hooked on the generic save_post, so guard the post type explicitly. if ( 'odw_dataset' !== get_post_type( $post_id ) ) { return; } - $result = self::calculate( $post_id ); - self::store( $post_id, $result ); + self::store( $post_id, self::calculate( $post_id ) ); } // ------------------------------------------------------------------------- @@ -351,25 +377,37 @@ public static function recalculate_on_save( int $post_id ): void { // ------------------------------------------------------------------------- /** - * Hängt Qualitätsdaten an den JSON-LD Dataset-Array an. + * Hängt die MQA-Qualitätsdaten an den JSON-LD Dataset-Array an. * - * @param array $dataset Der JSON-LD Array. - * @param int $post_id Post-ID. + * @param array $dataset Der JSON-LD Array. + * @param int $post_id Post-ID. * @return array */ public static function append_to_jsonld( array $dataset, int $post_id ): array { $quality = self::get( $post_id ); - if ( '' === $quality['level'] ) { + if ( empty( $quality['rating'] ) ) { return $dataset; } + $dimensions = array(); + foreach ( (array) ( $quality['dimensions'] ?? array() ) as $dim => $data ) { + $dimensions[ $dim ] = array( + 'odw:score' => (int) ( $data['achieved'] ?? 0 ), + 'odw:assessable' => (int) ( $data['assessable'] ?? 0 ), + 'odw:maxScore' => (int) ( $data['max'] ?? 0 ), + ); + } + $dataset['odw:qualityScore'] = array( '@type' => 'odw:QualityScore', - 'odw:score' => $quality['score'], - 'odw:maxScore' => 100, - 'odw:level' => $quality['level'], - 'odw:calculatedAt' => $quality['calculated_at'], + 'odw:methodology' => 'https://data.europa.eu/mqa/methodology', + 'odw:score' => (int) ( $quality['achieved'] ?? 0 ), + 'odw:assessable' => (int) ( $quality['assessable'] ?? 0 ), + 'odw:maxScore' => (int) ( $quality['max'] ?? self::MQA_MAX ), + 'odw:rating' => (string) $quality['rating'], + 'odw:dimensions' => $dimensions, + 'odw:calculatedAt' => (string) ( $quality['calculated_at'] ?? '' ), ); return $dataset; @@ -385,7 +423,7 @@ public static function append_to_jsonld( array $dataset, int $post_id ): array { public static function register_meta_box(): void { add_meta_box( 'odw-quality-report', - __( 'Qualitätsprüfung', 'open-data-wizard' ), + __( 'Qualitätsprüfung (MQA)', 'open-data-wizard' ), array( self::class, 'render_meta_box' ), 'odw_dataset', 'normal', @@ -394,97 +432,128 @@ public static function register_meta_box(): void { } /** - * Rendert den Inhalt der Qualitäts-Meta-Box. + * Rendert den Inhalt der Qualitäts-Meta-Box (MQA-Dimensionen + Metriken). * * @param \WP_Post $post Current post object. */ public static function render_meta_box( \WP_Post $post ): void { - $quality = self::get( $post->ID ); - $indicators = self::get_indicators(); + $quality = self::get( $post->ID ); - if ( '' === $quality['level'] ) { + if ( empty( $quality['rating'] ) ) { echo '

' . esc_html__( 'Noch keine Qualitätsanalyse vorhanden. Datensatz speichern, um die Prüfung auszuführen.', 'open-data-wizard' ) . '

'; return; } - $score = $quality['score']; - $level = $quality['level']; - $level_label = self::get_level_label( $level ); - // Map 'perfect' to 'high' CSS class so existing styles apply. - $level_class = 'odw-quality--' . ( 'perfect' === $level ? 'high' : $level ); - $stored = $quality['indicators']; + $achieved = (int) ( $quality['achieved'] ?? 0 ); + $assessable = (int) ( $quality['assessable'] ?? 0 ); + $max = (int) ( $quality['max'] ?? self::MQA_MAX ); + $rating = (string) $quality['rating']; + $rating_label = self::get_rating_label( $rating ); + $level_class = 'odw-quality--' . self::rating_to_level( $rating ); + $percent = $assessable > 0 ? (int) round( $achieved / $assessable * 100 ) : 0; + $not_assessed = $max - $assessable; + $dim_labels = self::get_dimension_labels(); + $metrics = (array) ( $quality['metrics'] ?? array() ); ?>

- +

-
+ aria-valuemax="">
- / 100 + + + ' . esc_html( + sprintf( + /* translators: %d: MQA maximum score */ + __( '(von max. %d)', 'open-data-wizard' ), + $max + ) + ) . ''; + } + ?> +
- +
+ 0 ) : ?> +

+ +

+ + - - - - - - - '; - } elseif ( false === $required && null !== $prev_required ) { - $section = ( $pts >= 10 ) - ? __( 'Empfohlene Felder', 'open-data-wizard' ) - : __( 'Optionale Angaben', 'open-data-wizard' ); - echo ''; + foreach ( self::DIMENSIONS as $dim ) { + $dim_data = $quality['dimensions'][ $dim ] ?? array( + 'achieved' => 0, + 'assessable' => 0, + 'max' => 0, + ); + $d_ach = (int) $dim_data['achieved']; + $d_ass = (int) $dim_data['assessable']; + $d_max = (int) $dim_data['max']; + + echo ''; + + foreach ( $metrics as $m ) { + if ( ( $m['dimension'] ?? '' ) !== $dim ) { + continue; } - $prev_required = $required; - } elseif ( false === $required && $pts < 10 && $prev_pts >= 10 ) { - // Wechsel von Empfohlen zu Optional innerhalb derselben required=false Gruppe. - echo ''; - } - $prev_pts = $pts; - - $status_icon = $passed ? '✓' : '✗'; - $status_class = $passed ? 'odw-quality-pass' : 'odw-quality-fail'; - $pts_display = $passed ? $pts : "0 / {$pts}"; - ?> - - - - - - + + + + + + @@ -495,7 +564,7 @@ public static function render_meta_box( \WP_Post $post ): void { printf( /* translators: %s: Datetime of last quality calculation */ esc_html__( 'Letzte Berechnung: %s · Wird bei jedem Speichern aktualisiert.', 'open-data-wizard' ), - esc_html( $quality['calculated_at'] ) + esc_html( (string) ( $quality['calculated_at'] ?? '' ) ) ); ?>

@@ -508,9 +577,85 @@ public static function render_meta_box( \WP_Post $post ): void { // ------------------------------------------------------------------------- /** - * Returns a human-readable label for a quality level constant. + * Menschenlesbares Label einer MQA-Bewertungsstufe. + * + * @param string $rating Eine der RATING_* Konstanten. + * @return string Übersetztes Label. + */ + public static function get_rating_label( string $rating ): string { + return array( + self::RATING_EXCELLENT => __( 'Ausgezeichnet', 'open-data-wizard' ), + self::RATING_GOOD => __( 'Gut', 'open-data-wizard' ), + self::RATING_SUFFICIENT => __( 'Ausreichend', 'open-data-wizard' ), + self::RATING_BAD => __( 'Mangelhaft', 'open-data-wizard' ), + )[ $rating ] ?? __( 'Unbekannt', 'open-data-wizard' ); + } + + // ------------------------------------------------------------------------- + // Abwärtskompatibilität (Legacy-API für Admin-Spalte, Validierung, Tests) + // ------------------------------------------------------------------------- + + /** + * Gibt die DCAT-AP-Indikatoren (Validierungsregistry) zurück. + * Weiterhin aus config/dcat-ap-fields.php geladen — steuert die Publish-Validierung. + * + * @return array + */ + public static function get_indicators(): array { + if ( class_exists( 'ODW_Fields' ) ) { + $defs = ODW_Fields::load_field_definitions(); + if ( ! empty( $defs ) ) { + return $defs; + } + } + + return array(); + } + + /** + * Summe der Pflichtpunkte aus der Indikator-Config (Legacy-Schwelle). + * + * @return int + */ + private static function get_required_only_score(): int { + $sum = 0; + foreach ( self::get_indicators() as $indicator ) { + if ( ! empty( $indicator['required'] ) ) { + $sum += (int) $indicator['points']; + } + } + + $threshold = $sum > 0 ? $sum : self::REQUIRED_ONLY_SCORE; + + return min( 100, $threshold ); + } + + /** + * Legacy: ermittelt ein 4-stufiges Level aus einem 0–100-Score. + * Weiterhin von der Admin-Listenspalte verwendet. * - * @param string $level One of LEVEL_HIGH, LEVEL_MEDIUM, LEVEL_LOW. + * @param int $score Numeric score 0–100. + * @return string One of LEVEL_PERFECT, LEVEL_HIGH, LEVEL_SUFFICIENT, LEVEL_LOW. + */ + public static function get_level( int $score ): string { + $required_only = self::get_required_only_score(); + + if ( 100 === $score ) { + return self::LEVEL_PERFECT; + } + if ( $score > $required_only ) { + return self::LEVEL_HIGH; + } + if ( $required_only === $score ) { + return self::LEVEL_SUFFICIENT; + } + return self::LEVEL_LOW; + } + + /** + * Legacy-Label für eine Level-Konstante (Admin-Spalte). + * + * @param string $level One of LEVEL_*. * @return string Translated label. */ public static function get_level_label( string $level ): string { @@ -527,16 +672,21 @@ public static function get_level_label( string $level ): string { // ------------------------------------------------------------------------- /** - * Returns a zeroed-out quality result used when no data has been stored yet. + * Leeres MQA-Ergebnis (noch keine Analyse vorhanden). * - * @return array{score: int, level: string, indicators: array, calculated_at: string} + * @return array */ private static function empty_result(): array { return array( + 'achieved' => 0, + 'assessable' => 0, + 'max' => self::MQA_MAX, + 'rating' => '', + 'dimensions' => array(), + 'metrics' => array(), + 'calculated_at' => '', 'score' => 0, 'level' => '', - 'indicators' => array(), - 'calculated_at' => '', ); } } diff --git a/languages/open-data-wizard-en_US.mo b/languages/open-data-wizard-en_US.mo index 3fde209..0dd6d78 100644 Binary files a/languages/open-data-wizard-en_US.mo and b/languages/open-data-wizard-en_US.mo differ diff --git a/languages/open-data-wizard-en_US.po b/languages/open-data-wizard-en_US.po index c54641d..4e2f65c 100644 --- a/languages/open-data-wizard-en_US.po +++ b/languages/open-data-wizard-en_US.po @@ -15,9 +15,18 @@ msgstr "" msgid " erstellt und sind dann zur Bearbeitung verfügbar." msgstr " created and are then available for editing." +msgid "%1$d von %2$d Pflichtangaben ausgefüllt" +msgstr "%1$d of %2$d required fields completed" + +msgid "%d Punkte werden derzeit nicht bewertet (URL-Erreichbarkeit, Vokabular- und DCAT-AP-SHACL-Prüfung folgen)." +msgstr "%d points are currently not assessed (URL reachability, vocabulary and DCAT-AP SHACL checks are coming)." + msgid "(optional)" msgstr "(optional)" +msgid "(von max. %d)" +msgstr "(of max. %d)" + msgid "1 — Grundlegende Informationen" msgstr "1 — Basic Information" @@ -36,6 +45,9 @@ msgstr "5 — Preview" msgid "AKTUALISIERUNGSFREQUENZ (dct:accrualPeriodicity)" msgstr "UPDATE FREQUENCY (dct:accrualPeriodicity)" +msgid "AMTLICHER GEBIETSSCHLÜSSEL (dcatde:politicalGeocodingURI)" +msgstr "OFFICIAL TERRITORY KEY (dcatde:politicalGeocodingURI)" + msgid "Ab wann sind diese Daten gültig?" msgstr "From when are these data valid?" @@ -45,18 +57,30 @@ msgstr "Cancel" msgid "Abdeckung" msgstr "Coverage" +msgid "AccessURL erreichbar (dcat:accessURL)" +msgstr "AccessURL reachable (dcat:accessURL)" + +msgid "Aktualisiert sich automatisch, während Sie die Felder ausfüllen — ohne Speichern." +msgstr "Updates automatically as you fill in the fields — no saving required." + msgid "Alle Datensätze" msgstr "All Datasets" msgid "Alle Datensätze, Metadaten und Plugin-Optionen beim Deinstallieren unwiderruflich löschen." msgstr "Irreversibly delete all datasets, metadata and plugin options on uninstall." +msgid "Alle Pflichtangaben ausgefüllt" +msgstr "All required fields completed" + msgid "Alle Qualitätsscores neu berechnen" msgstr "Recalculate all quality scores" msgid "Alle Status" msgstr "All Statuses" +msgid "Als ISO-8601-Dauer. Beispiel: P1D (täglich), PT1H (stündlich)" +msgstr "As an ISO 8601 duration. Example: P1D (daily), PT1H (hourly)" + msgid "Als Vorschaubild verwenden" msgstr "Use as featured image" @@ -66,18 +90,36 @@ msgstr "At which administrative level were these data collected?" msgid "Auf welcher Website finde ich weitere Kontaktinformationen?" msgstr "On which website can I find further contact information?" +msgid "Auf welcher rechtlichen Grundlage werden die Daten bereitgestellt?" +msgstr "On what legal basis are the data provided?" + +msgid "Auf welches amtliche Gebiet beziehen sich die Daten?" +msgstr "Which official territory do the data refer to?" + +msgid "Auffindbarkeit" +msgstr "Findability" + msgid "Aus dem CESSDA Controlled Vocabulary (Version 4.2.3, Deutsch). Beispiel: Volkszählungen, Migration, Wirtschaftspolitik" msgstr "From the CESSDA Controlled Vocabulary (version 4.2.3, German). Example: censuses, migration, economic policy" +msgid "Ausgezeichnet" +msgstr "Excellent" + msgid "Ausreichend" msgstr "Sufficient" +msgid "Ausstellungsdatum (dct:issued)" +msgstr "Issue date (dct:issued)" + msgid "Auswählen" msgstr "Select" msgid "BESCHREIBUNG (dct:description)" msgstr "DESCRIPTION (dct:description)" +msgid "BESCHREIBUNG DER DISTRIBUTION (dct:description)" +msgstr "DISTRIBUTION DESCRIPTION (dct:description)" + msgid "Batch-Import" msgstr "Batch Import" @@ -135,6 +177,9 @@ msgstr "Ready to import! All selected datasets are created as " msgid "Beschreiben Sie Ihre Datensätze dort, wo Sie ohnehin arbeiten – direkt in WordPress. Das Plugin generiert daraus eine maschinenlesbare Beschreibung nach dem internationalen Standard DCAT-AP 3.0 und stellt sie unter einer persistenten URL bereit." msgstr "Describe your datasets right where you already work – directly in WordPress. The plugin generates a machine-readable description according to the international DCAT-AP 3.0 standard and makes it available at a persistent URL." +msgid "Beschreibung" +msgstr "Description" + msgid "Beschreibung (dct:description)" msgstr "Description (dct:description)" @@ -144,6 +189,9 @@ msgstr "Description of the dataset" msgid "Bevölkerung & Gesellschaft" msgstr "Population & Society" +msgid "Bewertung nach der EU-Metadata-Quality-Assessment-Methodik (data.europa.eu). Die Werte werden bei jedem Speichern neu berechnet – Änderungen im Formular wirken sich erst nach dem Speichern aus." +msgstr "Assessment according to the EU Metadata Quality Assessment methodology (data.europa.eu). The values are recalculated on every save – changes in the form only take effect after saving." + msgid "Bildung, Kultur & Sport" msgstr "Education, Culture & Sport" @@ -180,6 +228,12 @@ msgstr "CESSDA TOPIC CLASSIFICATION (cessda:topic)" msgid "CESSDA Themenklassifikation" msgstr "CESSDA Topic Classification" +msgid "CESSDA-THEMENKLASSIFIKATION (dct:subject)" +msgstr "CESSDA TOPIC CLASSIFICATION (dct:subject)" + +msgid "CONTRIBUTOR-ID (dcatde:contributorID)" +msgstr "CONTRIBUTOR ID (dcatde:contributorID)" + msgid "CSV oder JSON Datei" msgstr "CSV or JSON file" @@ -201,12 +255,21 @@ msgstr "Catalog endpoint (REST API)" msgid "Cleared %d transient cache(s)." msgstr "Cleared %d transient cache(s)." +msgid "DATENSATZ-TYP (dct:type)" +msgstr "DATASET TYPE (dct:type)" + msgid "DCAT-AP 3.0 Pflichtfelder" msgstr "DCAT-AP 3.0 required fields" msgid "DCAT-AP 3.0 konforme Datensatz-Metadaten" msgstr "DCAT-AP 3.0 compliant dataset metadata" +msgid "DCAT-AP-Konformität (SHACL)" +msgstr "DCAT-AP compliance (SHACL)" + +msgid "DOWNLOAD-URL (dcat:downloadURL)" +msgstr "DOWNLOAD URL (dcat:downloadURL)" + msgid "Das Problem" msgstr "The Problem" @@ -243,6 +306,9 @@ msgstr "File size" msgid "Dateigröße (Bytes)" msgstr "File size (bytes)" +msgid "Dateigröße (dcat:byteSize)" +msgstr "File size (dcat:byteSize)" + msgid "Dateigröße Zahlenwert" msgstr "File size numeric value" @@ -312,6 +378,9 @@ msgstr "The data are in the public domain. They may be copied, modified and reus msgid "Die Idee" msgstr "The Idea" +msgid "Die JSON-LD-Ansicht unten zeigt das zuletzt gespeicherte Ergebnis." +msgstr "The JSON-LD view below shows the last saved result." + msgid "Die Qualitätsprüfung bewertet die Vollständigkeit der Metadaten. Die Werte werden bei jedem Speichern neu berechnet – Änderungen im Formular wirken sich erst nach dem Speichern auf das Ergebnis aus." msgstr "The quality check evaluates the completeness of the metadata. The values are recalculated each time you save – changes in the form only affect the result after saving." @@ -327,6 +396,12 @@ msgstr "This dataset is not published." msgid "Dieser Demo-Datensatz enthält eine Beispielliste zivilgesellschaftlicher Organisationen. Er wurde automatisch bei der Plugin-Installation erstellt und kann als Vorlage oder zum Testen des [odw_dataset]-Shortcodes verwendet werden." msgstr "This demo dataset contains a sample list of civil society organizations. It was created automatically during plugin installation and can be used as a template or to test the [odw_dataset] shortcode." +msgid "Direkte URL zum Herunterladen der Datei (im Gegensatz zur Zugriffs-URL)." +msgstr "Direct URL to download the file (as opposed to the access URL)." + +msgid "Direkter Download-Link zur Datei?" +msgstr "Direct download link to the file?" + msgid "Distribution" msgstr "Distribution" @@ -336,24 +411,63 @@ msgstr "Distribution with URL (dcat:accessURL)" msgid "Distribution mit Zugriffs-URL (dcat:accessURL)" msgstr "Distribution with access URL (dcat:accessURL)" +msgid "Distribution — erweitert" +msgstr "Distribution — advanced" + msgid "Download-Datei (Mediathek)" msgstr "Download file (media library)" msgid "Download-URL, Format, Dateigröße, Lizenz und Zuschreibungstext." msgstr "Download URL, format, file size, license and attribution text." +msgid "DownloadURL erreichbar (dcat:downloadURL)" +msgstr "DownloadURL reachable (dcat:downloadURL)" + +msgid "DownloadURL gesetzt (dcat:downloadURL)" +msgstr "DownloadURL set (dcat:downloadURL)" + +msgid "E-MAIL DES ERSTELLERS (foaf:mbox)" +msgstr "CREATOR E-MAIL (foaf:mbox)" + +msgid "E-Mail der pflegenden Stelle" +msgstr "Maintainer e-mail" + +msgid "E-Mail des Urhebers" +msgstr "Originator e-mail" + +msgid "E-Mail-Adresse der pflegenden Stelle (optional)." +msgstr "Maintainer e-mail address (optional)." + +msgid "E-Mail-Adresse des Erstellers (optional)." +msgstr "Creator's e-mail address (optional)." + +msgid "E-Mail-Adresse des Urhebers (optional)." +msgstr "Originator e-mail address (optional)." + msgid "E-Mail-Adresse für Rückfragen." msgstr "Email address for inquiries." msgid "EIGENE LIZENZ-URI" msgstr "CUSTOM LICENSE URI" +msgid "ERSTELLER (dct:creator)" +msgstr "CREATOR (dct:creator)" + +msgid "EU-Datenthema eintippen oder auswählen…" +msgstr "Type or select an EU data theme…" + msgid "Ein Demo-Datensatz mit Beispiel-CSV wurde automatisch erstellt. Fügen Sie diesen Shortcode in einen Beitrag oder eine Seite ein, um die Download-Card anzuzeigen:" msgstr "A demo dataset with a sample CSV was created automatically. Insert this shortcode into a post or page to display the download card:" msgid "Ein Fehler ist aufgetreten." msgstr "An error occurred." +msgid "Eine der sechs EU-Themenkategorien. Beispiel: Georaum, Meteorologie, Mobilität" +msgstr "One of the six EU thematic categories. Example: Geospatial, Meteorological, Mobility" + +msgid "Eine eindeutige Kennung des Datensatzes (z. B. DOI oder interne ID). Beispiel: 10.1234/abcd" +msgstr "A unique identifier of the dataset (e.g. DOI or internal ID). Example: 10.1234/abcd" + msgid "Einheit" msgstr "Unit" @@ -393,6 +507,9 @@ msgstr "Draft" msgid "Entwürfe" msgstr "Drafts" +msgid "Erdbeobachtung und Umwelt" +msgstr "Earth observation and environment" + msgid "Erste Schritte" msgstr "First Steps" @@ -402,9 +519,21 @@ msgstr "Created:" msgid "Erweiterte Angaben" msgstr "Advanced details" +msgid "Erweiterte Angaben (DCAT-AP.de, HVD, Zugriffsrechte) anzeigen" +msgstr "Show advanced fields (DCAT-AP.de, HVD, access rights)" + +msgid "Erweiterte Angaben für Profis (DCAT-AP.de, HVD, Zugriffsrechte) anzeigen" +msgstr "Show advanced fields for pros (DCAT-AP.de, HVD, access rights)" + +msgid "Experimentell" +msgstr "Experimental" + msgid "FORMAT (dct:format)" msgstr "FORMAT (dct:format)" +msgid "Fachliche Einordnung Ihres Datensatzes aus dem CESSDA Controlled Vocabulary (Version 4.2.3, Deutsch). Tippen Sie ein Thema ein oder wählen Sie eines aus der Liste — die passende URI wird automatisch verknüpft. Beispiel: Volkszählungen, Migration, Wirtschaftspolitik." +msgstr "A subject classification of your dataset from the CESSDA Controlled Vocabulary (version 4.2.3, German). Type a topic or pick one from the list — the matching URI is linked automatically. Example: censuses, migration, economic policy." + msgid "Fehler gefunden:" msgstr "Errors found:" @@ -423,12 +552,24 @@ msgstr "Filter by theme" msgid "Folgende Pflichtfelder fehlen oder sind leer:" msgstr "The following required fields are missing or empty:" +msgid "Format" +msgstr "Format" + msgid "Format der Distribution (dct:format)" msgstr "Format of the distribution (dct:format)" +msgid "Format gesetzt (dct:format)" +msgstr "Format set (dct:format)" + msgid "Format nicht unterstützt (nur CSV oder JSON)." msgstr "Format not supported (CSV or JSON only)." +msgid "Format/Media-Type aus Vokabular" +msgstr "Format/media type from vocabulary" + +msgid "Freitext zur Entstehung und Herkunft der Daten." +msgstr "Free text on the origin and provenance of the data." + msgid "Füllen Sie die Pflichtfelder aus (mit * gekennzeichnet) und speichern Sie den Datensatz. Sie können ihn später noch bearbeiten. Jedes Feld hat hilfreiche Beispiele – schauen Sie in den Hilfetexten vorbei!" msgstr "Fill in the required fields (marked with *) and save the dataset. You can edit it later. Every field has helpful examples – take a look at the help texts!" @@ -441,6 +582,12 @@ msgstr "Municipality" msgid "Generiertes JSON-LD live einsehen und über die REST-API abrufen." msgstr "View the generated JSON-LD live and retrieve it via the REST API." +msgid "Georaum (Geospatial)" +msgstr "Geospatial" + +msgid "Gesetz/Verordnung, das die Bereitstellung regelt. Beispiel: § 12a EGovG" +msgstr "Law/regulation governing the provision. Example: § 12a EGovG" + msgid "Gesundheit" msgstr "Health" @@ -453,6 +600,18 @@ msgstr "Valid datasets:" msgid "HERAUSGEBENDE ORGANISATION (dct:publisher)" msgstr "PUBLISHING ORGANIZATION (dct:publisher)" +msgid "HERKUNFT (dct:provenance)" +msgstr "PROVENANCE (dct:provenance)" + +msgid "HIGH-VALUE-DATENSATZ (EU-Durchführungsverordnung 2023/138)" +msgstr "HIGH-VALUE DATASET (EU Implementing Regulation 2023/138)" + +msgid "HVD-KATEGORIE (dcatap:hvdCategory)" +msgstr "HVD CATEGORY (dcatap:hvdCategory)" + +msgid "HVD-Kategorie auswählen (dcatap:hvdCategory)" +msgstr "Select an HVD category (dcatap:hvdCategory)" + msgid "Harvest-Endpoint" msgstr "Harvest endpoint" @@ -468,9 +627,24 @@ msgstr "Publisher (dct:publisher)" msgid "Herunterladen" msgstr "Download" +msgid "High-Value-Datensatz (HVD)" +msgstr "High-Value Dataset (HVD)" + +msgid "Hilfe anzeigen" +msgstr "Show help" + msgid "Hinweis ausblenden" msgstr "Dismiss notice" +msgid "Hochwertige Datensätze (HVD) sind von der EU festgelegte Datensätze mit besonderem Nutzen für Wirtschaft und Gesellschaft. Falls zutreffend, wählen Sie unten die passende Kategorie." +msgstr "High-value datasets (HVD) are datasets defined by the EU with particular benefit to the economy and society. If applicable, select the matching category below." + +msgid "IANA-Media-Type als URI. Beispiel: https://www.iana.org/assignments/media-types/text/csv" +msgstr "IANA media type as a URI. Example: https://www.iana.org/assignments/media-types/text/csv" + +msgid "IDENTIFIER (dct:identifier)" +msgstr "IDENTIFIER (dct:identifier)" + msgid "Import abgeschlossen!" msgstr "Import complete!" @@ -495,6 +669,12 @@ msgstr "Indicator" msgid "Internationale Themen" msgstr "International Topics" +msgid "Interoperabilität" +msgstr "Interoperability" + +msgid "Ist dies ein hochwertiger Datensatz (HVD)?" +msgstr "Is this a high-value dataset (HVD)?" + msgid "JSON-Beispiel" msgstr "JSON example" @@ -504,6 +684,9 @@ msgstr "JSON file could not be read." msgid "JSON-Format ist ungültig." msgstr "JSON format is invalid." +msgid "Ja, dieser Datensatz ist ein High-Value-Datensatz" +msgstr "Yes, this dataset is a high-value dataset" + msgid "Jede Distribution benötigt eine Lizenzangabe (dct:license)" msgstr "Each distribution requires a license (dct:license)" @@ -522,6 +705,9 @@ msgstr "Justice & Security" msgid "Jährlich" msgstr "Yearly" +msgid "KONFORM ZU (dct:conformsTo)" +msgstr "CONFORMS TO (dct:conformsTo)" + msgid "Katalog" msgstr "Catalog" @@ -531,6 +717,9 @@ msgstr "Catalog description" msgid "Katalog-Titel" msgstr "Catalog title" +msgid "Kategorien (dcat:theme)" +msgstr "Categories (dcat:theme)" + msgid "Keine Berechtigung." msgstr "No permission." @@ -561,18 +750,33 @@ msgstr "No valid datasets found." msgid "Keine gültigen Zeilen gefunden." msgstr "No valid rows found." +msgid "Kleinste räumlich auflösbare Distanz in Metern (nur Zahl). Beispiel: 30" +msgstr "Smallest spatially resolvable distance in meters (number only). Example: 30" + msgid "Klicken zum Markieren" msgstr "Click to select" msgid "Kontakt" msgstr "Contact" +msgid "Kontaktpunkt (dcat:contactPoint)" +msgstr "Contact point (dcat:contactPoint)" + +msgid "Kontext" +msgstr "Context" + +msgid "Kurze Beschreibung der Änderungen gegenüber der Vorversion." +msgstr "Short description of the changes from the previous version." + msgid "Kurze Beschreibung des Datensatzes…" msgstr "Short description of the dataset…" msgid "Kurze Beschreibung des Katalogs — erscheint als dct:description in der REST-API-Antwort (Pflichtfeld gemäß DCAT-AP.de)." msgstr "Short description of the catalog — appears as dct:description in the REST API response (required field per DCAT-AP.de)." +msgid "Kurzer Titel der Distribution. Beispiel: Gesamtdaten als CSV" +msgstr "Short title of the distribution. Example: Full data as CSV" + msgid "LIZENZ (dct:license)" msgstr "LICENSE (dct:license)" @@ -594,30 +798,54 @@ msgstr "Last calculation: %s · Updated on every save." msgid "Liefert alle veröffentlichten Datensätze als dcat:Catalog (JSON-LD). Unterstützt:" msgstr "Returns all published datasets as dcat:Catalog (JSON-LD). Supports:" +msgid "Live-Vorschau" +msgstr "Live preview" + msgid "Lizenz" msgstr "License" msgid "Lizenz (dct:license)" msgstr "License (dct:license)" +msgid "Lizenz aus Vokabular" +msgstr "License from vocabulary" + msgid "Lizenz aus dem kontrollierten Vokabular" msgstr "License from the controlled vocabulary" +msgid "Lizenz gesetzt (dct:license)" +msgstr "License set (dct:license)" + msgid "Lizenz-URI eingeben oder auswählen" msgstr "Enter or select a license URI" +msgid "MEDIENTYP (dcat:mediaType)" +msgstr "MEDIA TYPE (dcat:mediaType)" + +msgid "Mangelhaft" +msgstr "Poor" + +msgid "Maschinenlesbares Format" +msgstr "Machine-readable format" + msgid "Maximale Anzahl von %d Datensätzen erreicht — weitere Elemente wurden ignoriert." msgstr "Maximum of %d datasets reached — further elements were ignored." msgid "Maximale Anzahl von %d Datensätzen erreicht — weitere Zeilen wurden ignoriert." msgstr "Maximum of %d datasets reached — further rows were ignored." +msgid "Media-Type gesetzt (dcat:mediaType)" +msgstr "Media type set (dcat:mediaType)" + msgid "Metadaten (JSON-LD)" msgstr "Metadata (JSON-LD)" msgid "Metadaten des Datenkatalogs — erscheinen in der REST-API-Antwort des Catalog-Endpoints." msgstr "Metadata of the data catalog — appears in the REST API response of the catalog endpoint." +msgid "Meteorologie" +msgstr "Meteorological" + msgid "Mindestens eine Distribution mit Zugriffs-URL (dcat:accessURL)" msgstr "At least one distribution with an access URL (dcat:accessURL)" @@ -627,6 +855,9 @@ msgstr "With the batch import you create several datasets at once instead of ent msgid "Mit welchen Stichworten finde ich diese Daten?" msgstr "Which keywords help find these data?" +msgid "Mobilität" +msgstr "Mobility" + msgid "Monatlich" msgstr "Monthly" @@ -642,6 +873,9 @@ msgstr "Name of the publishing organization" msgid "Name oder Organisation der Ansprechperson." msgstr "Name or organization of the contact person." +msgid "Nein" +msgstr "No" + msgid "Neuen Datensatz anlegen" msgstr "Create new dataset" @@ -651,6 +885,9 @@ msgstr "Create a new dataset" msgid "Neuer Datensatz" msgstr "New dataset" +msgid "Nicht-proprietäres Format" +msgstr "Non-proprietary format" + msgid "Noch keine Daten vorhanden. Bitte erst Pflichtfelder befüllen und speichern." msgstr "No data yet. Please fill in the required fields and save first." @@ -663,6 +900,9 @@ msgstr "Not yet calculated" msgid "Offene Daten zu veröffentlichen ist schwieriger als es sein müsste. Komplexe Formulare und unbekannte Fachbegriffe erschweren die Arbeit. Der Open Data Wizard vereinfacht dies, indem er Sie mit verständlichen Fragen durch den Prozess führt." msgstr "Publishing open data is harder than it needs to be. Complex forms and unfamiliar jargon make the work difficult. The Open Data Wizard simplifies this by guiding you through the process with easy-to-understand questions." +msgid "Offizielle Kennung der bereitstellenden Stelle aus dem DCAT-AP.de-Verzeichnis. Stelle aus der Liste wählen; die zugehörige offizielle URI wird automatisch verwendet." +msgstr "Official identifier of the providing body from the DCAT-AP.de registry. Choose a body from the list; the corresponding official URI is used automatically." + msgid "Open Data Wizard" msgstr "Open Data Wizard" @@ -690,9 +930,18 @@ msgstr "Open Data Wizard: Publishing blocked" msgid "Open-Data-Plattformen können diese URL als Harvest-Quelle einbinden und die Metadaten automatisch einsammeln." msgstr "Open data platforms can add this URL as a harvest source and collect the metadata automatically." +msgid "Optional ein weiteres Thema aus der offiziellen EU-Themenliste (ergänzt die Kategorie aus Tab 1)." +msgstr "Optionally an additional theme from the official EU theme list (supplements the category from tab 1)." + msgid "Optionale Angaben" msgstr "Optional details" +msgid "PFLEGENDE STELLE (dcatde:maintainer)" +msgstr "MAINTAINER (dcatde:maintainer)" + +msgid "PLANBARE VERFÜGBARKEIT (dcatap:availability)" +msgstr "PLANNED AVAILABILITY (dcatap:availability)" + msgid "PROJEKTSEITE (dcat:landingPage)" msgstr "PROJECT PAGE (dcat:landingPage)" @@ -717,18 +966,30 @@ msgstr "Project page, update frequency, geographic and temporal coverage, contac msgid "Punkte" msgstr "Points" +msgid "QUALITÄTSPROZESS (dcatde:qualityProcessURI)" +msgstr "QUALITY PROCESS (dcatde:qualityProcessURI)" + msgid "Qualität" msgstr "Quality" msgid "Qualitätsprüfung" msgstr "Quality Check" +msgid "Qualitätsprüfung (MQA)" +msgstr "Quality Check (MQA)" + msgid "Qualitätsscores aller vorhandenen Datensätze neu berechnen — z.B. nach Änderungen an der Scoring-Logik." msgstr "Recalculate quality scores of all existing datasets — e.g. after changes to the scoring logic." msgid "Qualitätsscores neu berechnet: %d Datensätze aktualisiert." msgstr "Quality scores recalculated: %d datasets updated." +msgid "RECHTE (dct:rights)" +msgstr "RIGHTS (dct:rights)" + +msgid "RECHTSGRUNDLAGE (dcatde:legalBasis)" +msgstr "LEGAL BASIS (dcatde:legalBasis)" + msgid "REST API" msgstr "REST API" @@ -738,6 +999,9 @@ msgstr "Open REST endpoint" msgid "Recalculated quality scores for %d dataset(s)." msgstr "Recalculated quality scores for %d dataset(s)." +msgid "Rechte (dct:rights)" +msgstr "Rights (dct:rights)" + msgid "Region aus der Liste wählen (mit GeoNames verknüpft) oder Freitext/URI eingeben. Beispiel: Deutschland, Bayern, Berlin" msgstr "Select a region from the list (linked to GeoNames) or enter free text/URI. Example: Germany, Bavaria, Berlin" @@ -747,6 +1011,12 @@ msgstr "Select a region from the list or type…" msgid "Regionen & Städte" msgstr "Regions & Cities" +msgid "RÄUMLICHE AUFLÖSUNG IN METERN (dcat:spatialResolutionInMeters)" +msgstr "SPATIAL RESOLUTION IN METERS (dcat:spatialResolutionInMeters)" + +msgid "Räumliche Abdeckung (dct:spatial)" +msgstr "Spatial coverage (dct:spatial)" + msgid "SCHLAGWORTE (dcat:keyword)" msgstr "KEYWORDS (dcat:keyword)" @@ -756,6 +1026,9 @@ msgstr "LANGUAGE (dct:language)" msgid "Schlagworte (dcat:keyword)" msgstr "Keywords (dcat:keyword)" +msgid "Schlüsselwörter (dcat:keyword)" +msgstr "Keywords (dcat:keyword)" + msgid "Sekunden" msgstr "Seconds" @@ -771,12 +1044,18 @@ msgstr "Other license…" msgid "Speichern Sie den Datensatz, um die Vorschau zu aktualisieren." msgstr "Save the dataset to update the preview." +msgid "Speichern Sie den Datensatz, um sie zu aktualisieren." +msgstr "Save the dataset to update it." + msgid "Sprache (dct:language)" msgstr "Language (dct:language)" msgid "Sprache, Schlagworte, Veröffentlichungs- und Änderungsdatum, Themenklassifikation." msgstr "Language, keywords, publication and modification date, topic classification." +msgid "Stabil (langfristig verfügbar)" +msgstr "Stable (available long-term)" + msgid "Standard-Lizenz" msgstr "Default license" @@ -786,18 +1065,39 @@ msgstr "Default language" msgid "Standardwerte für neue Datensätze" msgstr "Default values for new datasets" +msgid "Statistik" +msgstr "Statistics" + msgid "Status" msgstr "Status" +msgid "Stelle eintippen oder auswählen…" +msgstr "Type or select a body…" + +msgid "Stelle oder Person, die die Daten erstellt hat (kann vom Herausgeber abweichen)." +msgstr "Body or person that created the data (may differ from the publisher)." + +msgid "Stelle, die für die laufende Pflege/Aktualisierung der Daten zuständig ist." +msgstr "The body responsible for the ongoing maintenance/updating of the data." + +msgid "Stelle, von der die Daten ursprünglich stammen (kann von Herausgeber abweichen)." +msgstr "The body the data originally comes from (may differ from the publisher)." + msgid "THEMA (dcat:theme)" msgstr "THEME (dcat:theme)" +msgid "TITEL DER DISTRIBUTION (dct:title)" +msgstr "DISTRIBUTION TITLE (dct:title)" + msgid "Tab 4 enthält optionale DCAT-AP Felder: Projektseite (dcat:landingPage), Aktualisierungsfrequenz (dct:accrualPeriodicity), geographische und zeitliche Abdeckung sowie einen Kontaktpunkt (dcat:contactPoint)." msgstr "Tab 4 contains optional DCAT-AP fields: project page (dcat:landingPage), update frequency (dct:accrualPeriodicity), geographic and temporal coverage, and a contact point (dcat:contactPoint)." msgid "Tab 5 zeigt das generierte JSON-LD nach dem Speichern. Dort finden Sie auch den direkten Link zum REST-Endpoint." msgstr "Tab 5 shows the generated JSON-LD after saving. There you will also find the direct link to the REST endpoint." +msgid "Temporär (kann jederzeit entfallen)" +msgstr "Temporary (may be removed at any time)" + msgid "Thema" msgstr "Theme" @@ -819,13 +1119,34 @@ msgstr "Title of the dataset (WordPress title field)" msgid "Täglich" msgstr "Daily" -msgid "URL der Projektwebsite oder des Datenportals mit weiteren Informationen zum Datensatz. Beispiel: https://beispiel.de/projekt" -msgstr "URL of the project website or data portal with further information about the dataset. Example: https://example.com/project" +msgid "URHEBER (dcatde:originator)" +msgstr "ORIGINATOR (dcatde:originator)" -msgid "Umwelt" -msgstr "Environment" +msgid "URI aus der EU-Liste „dataset-type\". Beispiel: http://publications.europa.eu/resource/authority/dataset-type/STATISTICAL" +msgstr "URI from the EU \"dataset-type\" list. Example: http://publications.europa.eu/resource/authority/dataset-type/STATISTICAL" -msgid "Unbekannt" +msgid "URI des Standards/Anwendungsprofils, dem die Daten folgen." +msgstr "URI of the standard/application profile the data follows." + +msgid "URI des amtlichen Regional-/Gemeindeschlüssels (AGS/ARS). Beispiel: http://dcat-ap.de/def/politicalGeocoding/regionalKey/09" +msgstr "URI of the official regional/municipality key (AGS/ARS). Example: http://dcat-ap.de/def/politicalGeocoding/regionalKey/09" + +msgid "URI oder Freitext zu den Nutzungsrechten der Distribution." +msgstr "URI or free text on the usage rights of the distribution." + +msgid "URL der Projektwebsite oder des Datenportals mit weiteren Informationen zum Datensatz. Beispiel: https://beispiel.de/projekt" +msgstr "URL of the project website or data portal with further information about the dataset. Example: https://example.com/project" + +msgid "URL zur Beschreibung des Qualitätssicherungs-Prozesses (optional)." +msgstr "URL describing the quality-assurance process (optional)." + +msgid "Um welchen Typ von Datensatz handelt es sich?" +msgstr "What type of dataset is this?" + +msgid "Umwelt" +msgstr "Environment" + +msgid "Unbekannt" msgstr "Unknown" msgid "Ungültiger Dateityp (nur CSV oder JSON)." @@ -843,21 +1164,39 @@ msgstr "At which email address can I ask questions?" msgid "Unter welcher Lizenz sind diese Daten verfügbar?" msgstr "Under which license are these data available?" +msgid "Unternehmen und Eigentümerschaft von Unternehmen" +msgstr "Companies and company ownership" + +msgid "VERSION (owl:versionInfo)" +msgstr "VERSION (owl:versionInfo)" + +msgid "VERSIONSHINWEISE (adms:versionNotes)" +msgstr "VERSION NOTES (adms:versionNotes)" + msgid "VERWALTUNGSEBENE (dcatde:politicalGeocodingLevelURI)" msgstr "ADMINISTRATIVE LEVEL (dcatde:politicalGeocodingLevelURI)" msgid "VERÖFFENTLICHUNGSDATUM (dct:issued)" msgstr "PUBLICATION DATE (dct:issued)" +msgid "Verantwortlichkeiten & Herkunft" +msgstr "Responsibilities & Provenance" + msgid "Verbesserungsbedarf" msgstr "Needs improvement" +msgid "Verfügbar (mittelfristige Planung)" +msgstr "Available (medium-term planning)" + msgid "Verkehr" msgstr "Transport" msgid "Verknüpfte URI:" msgstr "Linked URI:" +msgid "Versionsbezeichnung des Datensatzes. Beispiel: 1.0, 2024-Q1" +msgstr "Version label of the dataset. Example: 1.0, 2024-Q1" + msgid "Verwaltung & öffentlicher Sektor" msgstr "Government & Public Sector" @@ -894,39 +1233,102 @@ msgstr "When were these data last updated?" msgid "Wann wurden diese Daten zum ersten Mal veröffentlicht?" msgstr "When were these data first published?" +msgid "Was hat sich in dieser Version geändert?" +msgstr "What changed in this version?" + msgid "Website mit weiteren Kontaktinformationen." msgstr "Website with further contact information." +msgid "Weitere DCAT-AP-Felder (optional)" +msgstr "More DCAT-AP fields (optional)" + msgid "Weitere Informationen:" msgstr "Further information:" +msgid "Weiteres EU-Thema (Kategorie-URI)?" +msgstr "Additional EU theme (category URI)?" + +msgid "Welche Nutzungsrechte gelten für die Datei?" +msgstr "What usage rights apply to the file?" + +msgid "Welche Stelle stellt diese Daten im GovData-Verbund bereit?" +msgstr "Which body provides this data within the GovData network?" + +msgid "Welche Version hat dieser Datensatz?" +msgstr "What version is this dataset?" + +msgid "Welche eindeutige Kennung hat dieser Datensatz?" +msgstr "What unique identifier does this dataset have?" + msgid "Welche geografische Region betreffen diese Daten?" msgstr "Which geographic region do these data concern?" +msgid "Welche räumliche Auflösung haben die Daten (in Metern)?" +msgstr "What spatial resolution does the data have (in meters)?" + +msgid "Welche zeitliche Auflösung haben die Daten?" +msgstr "What temporal resolution does the data have?" + +msgid "Welchem Standard oder Schema entsprechen die Daten?" +msgstr "Which standard or schema does the data conform to?" + +msgid "Welchen Medientyp (MIME) hat die Datei?" +msgstr "What media type (MIME) does the file have?" + +msgid "Welcher HVD-Kategorie gehört dieser Datensatz an?" +msgstr "Which HVD category does this dataset belong to?" + msgid "Welcher Namensnennungstext soll bei Weiternutzung angegeben werden?" msgstr "What attribution text should be given for reuse?" +msgid "Wer darf auf diese Daten zugreifen?" +msgstr "Who may access this data?" + msgid "Wer gibt diese Daten heraus?" msgstr "Who publishes these data?" msgid "Wer gibt diese Daten heraus? Worum geht es? In welche Kategorie gehört der Datensatz?" msgstr "Who publishes these data? What are they about? Which category does the dataset belong to?" +msgid "Wer hat diese Daten erstellt?" +msgstr "Who created this data?" + +msgid "Wer hat diese Daten ursprünglich erstellt?" +msgstr "Who originally created this data?" + msgid "Wer ist Ansprechperson für Fragen zu diesen Daten?" msgstr "Who is the contact person for questions about these data?" +msgid "Wer pflegt diese Daten laufend?" +msgstr "Who maintains this data on an ongoing basis?" + msgid "Wie CC BY: Nutzung und Bearbeitung erlaubt mit Namensnennung. Zusätzlich müssen bearbeitete Daten unter derselben Lizenz weitergegeben werden." msgstr "Like CC BY: use and modification allowed with attribution. In addition, modified data must be shared under the same license." +msgid "Wie dauerhaft ist diese Datei verfügbar?" +msgstr "How durably is this file available?" + msgid "Wie funktioniert es?" msgstr "How does it work?" +msgid "Wie heißt die bereitgestellte Datei/Distribution?" +msgstr "What is the name of the provided file/distribution?" + msgid "Wie lange Catalog- und Dataset-Antworten gecacht werden. Standard: 300 (5 Minuten). Min: 60, Max: 86400 (24 h)." msgstr "How long catalog and dataset responses are cached. Default: 300 (5 minutes). Min: 60, Max: 86400 (24 h)." +msgid "Wie lässt sich die Distribution beschreiben?" +msgstr "How can the distribution be described?" + msgid "Wie oft werden diese Daten aktualisiert?" msgstr "How often are these data updated?" +msgid "Wie verlässlich/dauerhaft ist der Zugriff auf diese Datei geplant? Beispiel: Stabil, Verfügbar, Temporär" +msgstr "How reliable/durable is access to this file planned to be? Example: Stable, Available, Temporary" + +msgid "Wiederverwendbarkeit" +msgstr "Reusability" + msgid "Willkommen im Open Data Wizard" msgstr "Welcome to the Open Data Wizard" @@ -945,15 +1347,24 @@ msgstr "Science & Technology" msgid "Wo finde ich mehr Informationen zu diesem Projekt?" msgstr "Where can I find more information about this project?" +msgid "Wo ist das Qualitätssicherungs-Verfahren dokumentiert?" +msgstr "Where is the quality-assurance process documented?" + msgid "Wo kann ich die Datei herunterladen?" msgstr "Where can I download the file?" +msgid "Woher stammen die Daten und wie sind sie entstanden?" +msgstr "Where does the data come from and how was it created?" + msgid "Worum geht es in diesem Datensatz?" msgstr "What is this dataset about?" msgid "Wöchentlich" msgstr "Weekly" +msgid "ZEITLICHE AUFLÖSUNG (dcat:temporalResolution)" +msgstr "TEMPORAL RESOLUTION (dcat:temporalResolution)" + msgid "ZEITLICHER BEZUG — ENDE (dct:temporal)" msgstr "TEMPORAL COVERAGE — END (dct:temporal)" @@ -963,6 +1374,12 @@ msgstr "TEMPORAL COVERAGE — START (dct:temporal)" msgid "ZUGRIFFS-URL (dcat:accessURL)" msgstr "ACCESS URL (dcat:accessURL)" +msgid "ZUGRIFFSRECHTE (dct:accessRights)" +msgstr "ACCESS RIGHTS (dct:accessRights)" + +msgid "ZUSÄTZLICHES THEMA (dcat:theme)" +msgstr "ADDITIONAL THEME (dcat:theme)" + msgid "Zeile %1$d: Dateigröße muss eine ganze Zahl in Bytes sein: %2$s" msgstr "Row %1$d: file size must be a whole number in bytes: %2$s" @@ -981,42 +1398,90 @@ msgstr "Row %d: could not create post." msgid "Zeile %d: Spaltenanzahl stimmt nicht überein." msgstr "Row %d: column count does not match." +msgid "Zeitliche Abdeckung (dct:temporal)" +msgstr "Temporal coverage (dct:temporal)" + msgid "Zu diesem Datensatz hochgeladen" msgstr "Uploaded to this dataset" +msgid "Zugriff & weitere Auffindbarkeit" +msgstr "Access & further discoverability" + msgid "Zugriff verweigert." msgstr "Access denied." +msgid "Zugriffs-Klassifikation des Datensatzes. Beispiel: Öffentlich, Eingeschränkt, Nicht öffentlich" +msgstr "Access classification of the dataset. Example: Public, Restricted, Non-public" + +msgid "Zugriffs-URL" +msgstr "Access URL" + +msgid "Zugriffsrechte aus Vokabular" +msgstr "Access rights from vocabulary" + +msgid "Zugriffsrechte gesetzt (dct:accessRights)" +msgstr "Access rights set (dct:accessRights)" + +msgid "Zugänglichkeit" +msgstr "Accessibility" + msgid "Zur Datensatzliste" msgstr "To the dataset list" +msgid "Zusammenfassung" +msgstr "Summary" + msgid "Zweijährlich" msgstr "Biennial" msgid "https://example.org/meine-lizenz" msgstr "https://example.org/my-license" +msgid "noch leer" +msgstr "still empty" + msgid "optional – nur bei CC BY oder CC BY-SA" msgstr "optional – only for CC BY or CC BY-SA" +msgid "z. B. 1.0" +msgstr "e.g. 1.0" + msgid "z. B. 2.5" msgstr "e.g. 2.5" +msgid "z. B. 30" +msgstr "e.g. 30" + +msgid "z. B. DOI oder interne ID" +msgstr "e.g. DOI or internal ID" + +msgid "z. B. Statistisches Amt" +msgstr "e.g. Statistical Office" + msgid "z.B. Musterorganisation e.V." msgstr "e.g. Sample Organization" msgid "z.B. Open Data Team" msgstr "e.g. Open Data Team" +msgid "z.B. Statistisches Landesamt" +msgstr "e.g. State Statistical Office" + msgid "z.B. Umwelt" msgstr "e.g. environment" +msgid "z.B. § 12a EGovG, Informationsweiterverwendungsgesetz (IWG)" +msgstr "e.g. § 12a EGovG, German Re-use of Public Sector Information Act (IWG)" + msgid "ÄNDERUNGSDATUM (dct:modified)" msgstr "MODIFICATION DATE (dct:modified)" msgid "Änderungsdatum" msgstr "Modification date" +msgid "Änderungsdatum (dct:modified)" +msgstr "Modification date (dct:modified)" + msgid "— Bitte wählen —" msgstr "— Please select —" @@ -1026,368 +1491,3 @@ msgstr "— No default —" msgid "💾 Unterstützte Formate: CSV, JSON • Maximale Größe: 10MB" msgstr "💾 Supported formats: CSV, JSON • Maximum size: 10MB" -msgid "Nein" -msgstr "No" - -msgid "Ja, dieser Datensatz ist ein High-Value-Datensatz" -msgstr "Yes, this dataset is a high-value dataset" - -msgid "High-Value-Datensatz (HVD)" -msgstr "High-Value Dataset (HVD)" - -msgid "Ist dies ein hochwertiger Datensatz (HVD)?" -msgstr "Is this a high-value dataset (HVD)?" - -msgid "HIGH-VALUE-DATENSATZ (EU-Durchführungsverordnung 2023/138)" -msgstr "HIGH-VALUE DATASET (EU Implementing Regulation 2023/138)" - -msgid "Hochwertige Datensätze (HVD) sind von der EU festgelegte Datensätze mit besonderem Nutzen für Wirtschaft und Gesellschaft. Falls zutreffend, wählen Sie unten die passende Kategorie." -msgstr "High-value datasets (HVD) are datasets defined by the EU with particular benefit to the economy and society. If applicable, select the matching category below." - -msgid "Welcher HVD-Kategorie gehört dieser Datensatz an?" -msgstr "Which HVD category does this dataset belong to?" - -msgid "HVD-KATEGORIE (dcatap:hvdCategory)" -msgstr "HVD CATEGORY (dcatap:hvdCategory)" - -msgid "Eine der sechs EU-Themenkategorien. Beispiel: Georaum, Meteorologie, Mobilität" -msgstr "One of the six EU thematic categories. Example: Geospatial, Meteorological, Mobility" - -msgid "Georaum (Geospatial)" -msgstr "Geospatial" - -msgid "Erdbeobachtung und Umwelt" -msgstr "Earth observation and environment" - -msgid "Meteorologie" -msgstr "Meteorological" - -msgid "Statistik" -msgstr "Statistics" - -msgid "Unternehmen und Eigentümerschaft von Unternehmen" -msgstr "Companies and company ownership" - -msgid "Mobilität" -msgstr "Mobility" - -msgid "HVD-Kategorie auswählen (dcatap:hvdCategory)" -msgstr "Select an HVD category (dcatap:hvdCategory)" - -msgid "Wie dauerhaft ist diese Datei verfügbar?" -msgstr "How durably is this file available?" - -msgid "PLANBARE VERFÜGBARKEIT (dcatap:availability)" -msgstr "PLANNED AVAILABILITY (dcatap:availability)" - -msgid "Wie verlässlich/dauerhaft ist der Zugriff auf diese Datei geplant? Beispiel: Stabil, Verfügbar, Temporär" -msgstr "How reliable/durable is access to this file planned to be? Example: Stable, Available, Temporary" - -msgid "Verfügbar (mittelfristige Planung)" -msgstr "Available (medium-term planning)" - -msgid "Stabil (langfristig verfügbar)" -msgstr "Stable (available long-term)" - -msgid "Experimentell" -msgstr "Experimental" - -msgid "Temporär (kann jederzeit entfallen)" -msgstr "Temporary (may be removed at any time)" - -msgid "Verantwortlichkeiten & Herkunft" -msgstr "Responsibilities & Provenance" - -msgid "Welche Stelle stellt diese Daten im GovData-Verbund bereit?" -msgstr "Which body provides this data within the GovData network?" - -msgid "Stelle eintippen oder auswählen…" -msgstr "Type or select a body…" - -msgid "CONTRIBUTOR-ID (dcatde:contributorID)" -msgstr "CONTRIBUTOR ID (dcatde:contributorID)" - -msgid "Offizielle Kennung der bereitstellenden Stelle aus dem DCAT-AP.de-Verzeichnis. Stelle aus der Liste wählen; die zugehörige offizielle URI wird automatisch verwendet." -msgstr "Official identifier of the providing body from the DCAT-AP.de registry. Choose a body from the list; the corresponding official URI is used automatically." - -msgid "Wer hat diese Daten ursprünglich erstellt?" -msgstr "Who originally created this data?" - -msgid "z.B. Statistisches Landesamt" -msgstr "e.g. State Statistical Office" - -msgid "URHEBER (dcatde:originator)" -msgstr "ORIGINATOR (dcatde:originator)" - -msgid "Stelle, von der die Daten ursprünglich stammen (kann von Herausgeber abweichen)." -msgstr "The body the data originally comes from (may differ from the publisher)." - -msgid "E-Mail des Urhebers" -msgstr "Originator e-mail" - -msgid "E-Mail-Adresse des Urhebers (optional)." -msgstr "Originator e-mail address (optional)." - -msgid "Wer pflegt diese Daten laufend?" -msgstr "Who maintains this data on an ongoing basis?" - -msgid "PFLEGENDE STELLE (dcatde:maintainer)" -msgstr "MAINTAINER (dcatde:maintainer)" - -msgid "Stelle, die für die laufende Pflege/Aktualisierung der Daten zuständig ist." -msgstr "The body responsible for the ongoing maintenance/updating of the data." - -msgid "E-Mail der pflegenden Stelle" -msgstr "Maintainer e-mail" - -msgid "E-Mail-Adresse der pflegenden Stelle (optional)." -msgstr "Maintainer e-mail address (optional)." - -msgid "Auf welches amtliche Gebiet beziehen sich die Daten?" -msgstr "Which official territory do the data refer to?" - -msgid "AMTLICHER GEBIETSSCHLÜSSEL (dcatde:politicalGeocodingURI)" -msgstr "OFFICIAL TERRITORY KEY (dcatde:politicalGeocodingURI)" - -msgid "URI des amtlichen Regional-/Gemeindeschlüssels (AGS/ARS). Beispiel: http://dcat-ap.de/def/politicalGeocoding/regionalKey/09" -msgstr "URI of the official regional/municipality key (AGS/ARS). Example: http://dcat-ap.de/def/politicalGeocoding/regionalKey/09" - -msgid "Auf welcher rechtlichen Grundlage werden die Daten bereitgestellt?" -msgstr "On what legal basis are the data provided?" - -msgid "z.B. § 12a EGovG, Informationsweiterverwendungsgesetz (IWG)" -msgstr "e.g. § 12a EGovG, German Re-use of Public Sector Information Act (IWG)" - -msgid "RECHTSGRUNDLAGE (dcatde:legalBasis)" -msgstr "LEGAL BASIS (dcatde:legalBasis)" - -msgid "Gesetz/Verordnung, das die Bereitstellung regelt. Beispiel: § 12a EGovG" -msgstr "Law/regulation governing the provision. Example: § 12a EGovG" - -msgid "Wo ist das Qualitätssicherungs-Verfahren dokumentiert?" -msgstr "Where is the quality-assurance process documented?" - -msgid "QUALITÄTSPROZESS (dcatde:qualityProcessURI)" -msgstr "QUALITY PROCESS (dcatde:qualityProcessURI)" - -msgid "URL zur Beschreibung des Qualitätssicherungs-Prozesses (optional)." -msgstr "URL describing the quality-assurance process (optional)." - -msgid "Zugriff & weitere Auffindbarkeit" -msgstr "Access & further discoverability" - -msgid "Wer darf auf diese Daten zugreifen?" -msgstr "Who may access this data?" - -msgid "ZUGRIFFSRECHTE (dct:accessRights)" -msgstr "ACCESS RIGHTS (dct:accessRights)" - -msgid "Zugriffs-Klassifikation des Datensatzes. Beispiel: Öffentlich, Eingeschränkt, Nicht öffentlich" -msgstr "Access classification of the dataset. Example: Public, Restricted, Non-public" - -msgid "Weiteres EU-Thema (Kategorie-URI)?" -msgstr "Additional EU theme (category URI)?" - -msgid "EU-Datenthema eintippen oder auswählen…" -msgstr "Type or select an EU data theme…" - -msgid "ZUSÄTZLICHES THEMA (dcat:theme)" -msgstr "ADDITIONAL THEME (dcat:theme)" - -msgid "Optional ein weiteres Thema aus der offiziellen EU-Themenliste (ergänzt die Kategorie aus Tab 1)." -msgstr "Optionally an additional theme from the official EU theme list (supplements the category from tab 1)." - -msgid "Erweiterte Angaben für Profis (DCAT-AP.de, HVD, Zugriffsrechte) anzeigen" -msgstr "Show advanced fields for pros (DCAT-AP.de, HVD, access rights)" - -msgid "Beschreibung" -msgstr "Description" - -msgid "Zugriffs-URL" -msgstr "Access URL" - -msgid "Format" -msgstr "Format" - -msgid "Hilfe anzeigen" -msgstr "Show help" - -msgid "noch leer" -msgstr "still empty" - -msgid "Live-Vorschau" -msgstr "Live preview" - -msgid "Zusammenfassung" -msgstr "Summary" - -msgid "Alle Pflichtangaben ausgefüllt" -msgstr "All required fields completed" - -msgid "%1$d von %2$d Pflichtangaben ausgefüllt" -msgstr "%1$d of %2$d required fields completed" - -msgid "Aktualisiert sich automatisch, während Sie die Felder ausfüllen — ohne Speichern." -msgstr "Updates automatically as you fill in the fields — no saving required." - -msgid "Die JSON-LD-Ansicht unten zeigt das zuletzt gespeicherte Ergebnis." -msgstr "The JSON-LD view below shows the last saved result." - -msgid "Speichern Sie den Datensatz, um sie zu aktualisieren." -msgstr "Save the dataset to update it." - -msgid "CESSDA-THEMENKLASSIFIKATION (dct:subject)" -msgstr "CESSDA TOPIC CLASSIFICATION (dct:subject)" - -msgid "Fachliche Einordnung Ihres Datensatzes aus dem CESSDA Controlled Vocabulary (Version 4.2.3, Deutsch). Tippen Sie ein Thema ein oder wählen Sie eines aus der Liste — die passende URI wird automatisch verknüpft. Beispiel: Volkszählungen, Migration, Wirtschaftspolitik." -msgstr "A subject classification of your dataset from the CESSDA Controlled Vocabulary (version 4.2.3, German). Type a topic or pick one from the list — the matching URI is linked automatically. Example: censuses, migration, economic policy." - -msgid "Erweiterte Angaben (DCAT-AP.de, HVD, Zugriffsrechte) anzeigen" -msgstr "Show advanced fields (DCAT-AP.de, HVD, access rights)" - -msgid "Weitere DCAT-AP-Felder (optional)" -msgstr "More DCAT-AP fields (optional)" - -msgid "Distribution — erweitert" -msgstr "Distribution — advanced" - -msgid "Welche eindeutige Kennung hat dieser Datensatz?" -msgstr "What unique identifier does this dataset have?" - -msgid "z. B. DOI oder interne ID" -msgstr "e.g. DOI or internal ID" - -msgid "IDENTIFIER (dct:identifier)" -msgstr "IDENTIFIER (dct:identifier)" - -msgid "Eine eindeutige Kennung des Datensatzes (z. B. DOI oder interne ID). Beispiel: 10.1234/abcd" -msgstr "A unique identifier of the dataset (e.g. DOI or internal ID). Example: 10.1234/abcd" - -msgid "Um welchen Typ von Datensatz handelt es sich?" -msgstr "What type of dataset is this?" - -msgid "DATENSATZ-TYP (dct:type)" -msgstr "DATASET TYPE (dct:type)" - -msgid "URI aus der EU-Liste „dataset-type\". Beispiel: http://publications.europa.eu/resource/authority/dataset-type/STATISTICAL" -msgstr "URI from the EU \"dataset-type\" list. Example: http://publications.europa.eu/resource/authority/dataset-type/STATISTICAL" - -msgid "Wer hat diese Daten erstellt?" -msgstr "Who created this data?" - -msgid "z. B. Statistisches Amt" -msgstr "e.g. Statistical Office" - -msgid "ERSTELLER (dct:creator)" -msgstr "CREATOR (dct:creator)" - -msgid "Stelle oder Person, die die Daten erstellt hat (kann vom Herausgeber abweichen)." -msgstr "Body or person that created the data (may differ from the publisher)." - -msgid "E-Mail-Adresse des Erstellers (optional)." -msgstr "Creator's e-mail address (optional)." - -msgid "E-MAIL DES ERSTELLERS (foaf:mbox)" -msgstr "CREATOR E-MAIL (foaf:mbox)" - -msgid "Welche Version hat dieser Datensatz?" -msgstr "What version is this dataset?" - -msgid "z. B. 1.0" -msgstr "e.g. 1.0" - -msgid "VERSION (owl:versionInfo)" -msgstr "VERSION (owl:versionInfo)" - -msgid "Versionsbezeichnung des Datensatzes. Beispiel: 1.0, 2024-Q1" -msgstr "Version label of the dataset. Example: 1.0, 2024-Q1" - -msgid "Was hat sich in dieser Version geändert?" -msgstr "What changed in this version?" - -msgid "VERSIONSHINWEISE (adms:versionNotes)" -msgstr "VERSION NOTES (adms:versionNotes)" - -msgid "Kurze Beschreibung der Änderungen gegenüber der Vorversion." -msgstr "Short description of the changes from the previous version." - -msgid "Welche räumliche Auflösung haben die Daten (in Metern)?" -msgstr "What spatial resolution does the data have (in meters)?" - -msgid "z. B. 30" -msgstr "e.g. 30" - -msgid "RÄUMLICHE AUFLÖSUNG IN METERN (dcat:spatialResolutionInMeters)" -msgstr "SPATIAL RESOLUTION IN METERS (dcat:spatialResolutionInMeters)" - -msgid "Kleinste räumlich auflösbare Distanz in Metern (nur Zahl). Beispiel: 30" -msgstr "Smallest spatially resolvable distance in meters (number only). Example: 30" - -msgid "Welche zeitliche Auflösung haben die Daten?" -msgstr "What temporal resolution does the data have?" - -msgid "ZEITLICHE AUFLÖSUNG (dcat:temporalResolution)" -msgstr "TEMPORAL RESOLUTION (dcat:temporalResolution)" - -msgid "Als ISO-8601-Dauer. Beispiel: P1D (täglich), PT1H (stündlich)" -msgstr "As an ISO 8601 duration. Example: P1D (daily), PT1H (hourly)" - -msgid "Welchem Standard oder Schema entsprechen die Daten?" -msgstr "Which standard or schema does the data conform to?" - -msgid "KONFORM ZU (dct:conformsTo)" -msgstr "CONFORMS TO (dct:conformsTo)" - -msgid "URI des Standards/Anwendungsprofils, dem die Daten folgen." -msgstr "URI of the standard/application profile the data follows." - -msgid "Woher stammen die Daten und wie sind sie entstanden?" -msgstr "Where does the data come from and how was it created?" - -msgid "HERKUNFT (dct:provenance)" -msgstr "PROVENANCE (dct:provenance)" - -msgid "Freitext zur Entstehung und Herkunft der Daten." -msgstr "Free text on the origin and provenance of the data." - -msgid "Wie heißt die bereitgestellte Datei/Distribution?" -msgstr "What is the name of the provided file/distribution?" - -msgid "TITEL DER DISTRIBUTION (dct:title)" -msgstr "DISTRIBUTION TITLE (dct:title)" - -msgid "Kurzer Titel der Distribution. Beispiel: Gesamtdaten als CSV" -msgstr "Short title of the distribution. Example: Full data as CSV" - -msgid "Wie lässt sich die Distribution beschreiben?" -msgstr "How can the distribution be described?" - -msgid "BESCHREIBUNG DER DISTRIBUTION (dct:description)" -msgstr "DISTRIBUTION DESCRIPTION (dct:description)" - -msgid "Direkter Download-Link zur Datei?" -msgstr "Direct download link to the file?" - -msgid "DOWNLOAD-URL (dcat:downloadURL)" -msgstr "DOWNLOAD URL (dcat:downloadURL)" - -msgid "Direkte URL zum Herunterladen der Datei (im Gegensatz zur Zugriffs-URL)." -msgstr "Direct URL to download the file (as opposed to the access URL)." - -msgid "Welchen Medientyp (MIME) hat die Datei?" -msgstr "What media type (MIME) does the file have?" - -msgid "MEDIENTYP (dcat:mediaType)" -msgstr "MEDIA TYPE (dcat:mediaType)" - -msgid "IANA-Media-Type als URI. Beispiel: https://www.iana.org/assignments/media-types/text/csv" -msgstr "IANA media type as a URI. Example: https://www.iana.org/assignments/media-types/text/csv" - -msgid "Welche Nutzungsrechte gelten für die Datei?" -msgstr "What usage rights apply to the file?" - -msgid "RECHTE (dct:rights)" -msgstr "RIGHTS (dct:rights)" - -msgid "URI oder Freitext zu den Nutzungsrechten der Distribution." -msgstr "URI or free text on the usage rights of the distribution." diff --git a/open-data-wizard.php b/open-data-wizard.php index 14e7df4..e85fda3 100644 --- a/open-data-wizard.php +++ b/open-data-wizard.php @@ -3,7 +3,7 @@ * Plugin Name: Open Data Wizard * Plugin URI: https://github.com/daimpad/OpenDataWizard * Description: DCAT-AP 3.0 konforme Open Data Metadatenverwaltung für WordPress. Bereitstellung als maschinenlesbarer JSON-LD-Endpoint für offene Daten. - * Version: 2.12.0 + * Version: 2.13.0 * Requires at least: 6.4 * Requires PHP: 8.1 * Author: nozilla @@ -26,7 +26,7 @@ exit; } -define( 'ODW_VERSION', '2.12.0' ); +define( 'ODW_VERSION', '2.13.0' ); define( 'ODW_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'ODW_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); define( 'ODW_PLUGIN_FILE', __FILE__ ); diff --git a/tests/test-quality.php b/tests/test-quality.php index 47f2864..8121d26 100644 --- a/tests/test-quality.php +++ b/tests/test-quality.php @@ -187,92 +187,163 @@ public function test_get_indicators_has_title_key(): void { } // ------------------------------------------------------------------------- - // get() — liest aus Post-Meta + // get_rating() — MQA-Bewertungsstufen (proportional) // ------------------------------------------------------------------------- /** - * Returns a zeroed result when no quality level is stored in post meta. + * A full assessable score maps to the excellent rating. */ - public function test_get_returns_empty_result_when_no_level_stored(): void { + public function test_get_rating_full_is_excellent(): void { $this->load_class(); + \WP_Mock::userFunction( '__' )->andReturnArg( 0 ); + + $this->assertSame( ODW_Quality::RATING_EXCELLENT, ODW_Quality::get_rating( 230, 230 ) ); + $this->assertSame( ODW_Quality::RATING_EXCELLENT, ODW_Quality::get_rating( 405, 405 ) ); + } + + /** + * The proportional thresholds (351/221/121 of 405) map to the correct bands. + */ + public function test_get_rating_bands_on_full_scale(): void { + $this->load_class(); + \WP_Mock::userFunction( '__' )->andReturnArg( 0 ); + + $this->assertSame( ODW_Quality::RATING_EXCELLENT, ODW_Quality::get_rating( 351, 405 ) ); + $this->assertSame( ODW_Quality::RATING_GOOD, ODW_Quality::get_rating( 350, 405 ) ); + $this->assertSame( ODW_Quality::RATING_GOOD, ODW_Quality::get_rating( 221, 405 ) ); + $this->assertSame( ODW_Quality::RATING_SUFFICIENT, ODW_Quality::get_rating( 220, 405 ) ); + $this->assertSame( ODW_Quality::RATING_SUFFICIENT, ODW_Quality::get_rating( 121, 405 ) ); + $this->assertSame( ODW_Quality::RATING_BAD, ODW_Quality::get_rating( 120, 405 ) ); + $this->assertSame( ODW_Quality::RATING_BAD, ODW_Quality::get_rating( 0, 405 ) ); + } + + /** + * A zero assessable maximum yields the bad rating without division by zero. + */ + public function test_get_rating_zero_assessable_is_bad(): void { + $this->load_class(); + \WP_Mock::userFunction( '__' )->andReturnArg( 0 ); + + $this->assertSame( ODW_Quality::RATING_BAD, ODW_Quality::get_rating( 0, 0 ) ); + } + + /** + * Each MQA rating constant has a non-empty label; unknown falls back. + */ + public function test_get_rating_label(): void { + $this->load_class(); + \WP_Mock::userFunction( '__' )->andReturnArg( 0 ); + + $this->assertSame( 'Ausgezeichnet', ODW_Quality::get_rating_label( ODW_Quality::RATING_EXCELLENT ) ); + $this->assertSame( 'Mangelhaft', ODW_Quality::get_rating_label( ODW_Quality::RATING_BAD ) ); + $this->assertSame( 'Unbekannt', ODW_Quality::get_rating_label( 'nope' ) ); + } + + // ------------------------------------------------------------------------- + // get_metrics() — MQA-Konfiguration + // ------------------------------------------------------------------------- + + /** + * The MQA metric points sum to 405 across the five dimensions. + */ + public function test_get_metrics_sum_to_405(): void { + $this->load_class(); + \WP_Mock::userFunction( '__' )->andReturnArg( 0 ); + + $total = array_sum( array_column( ODW_Quality::get_metrics(), 'points' ) ); + $this->assertSame( 405, $total ); + } + + /** + * Each dimension reaches its documented MQA maximum. + */ + public function test_get_metrics_dimension_maxima(): void { + $this->load_class(); + \WP_Mock::userFunction( '__' )->andReturnArg( 0 ); + + $by_dim = array(); + foreach ( ODW_Quality::get_metrics() as $m ) { + $by_dim[ $m['dimension'] ] = ( $by_dim[ $m['dimension'] ] ?? 0 ) + (int) $m['points']; + } + + $this->assertSame( 100, $by_dim['findability'] ); + $this->assertSame( 100, $by_dim['accessibility'] ); + $this->assertSame( 110, $by_dim['interoperability'] ); + $this->assertSame( 75, $by_dim['reusability'] ); + $this->assertSame( 20, $by_dim['contextuality'] ); + } + + // ------------------------------------------------------------------------- + // get() / store() — MQA-Persistierung + // ------------------------------------------------------------------------- + + /** + * Returns a zeroed result when no MQA data is stored in post meta. + */ + public function test_get_returns_empty_result_when_nothing_stored(): void { + $this->load_class(); + \WP_Mock::userFunction( '__' )->andReturnArg( 0 ); \WP_Mock::userFunction( 'get_post_meta' ) - ->with( 42, '_odw_quality_level', true ) + ->with( 42, '_odw_mqa', true ) ->andReturn( '' ); $result = ODW_Quality::get( 42 ); $this->assertSame( 0, $result['score'] ); $this->assertSame( '', $result['level'] ); - $this->assertSame( array(), $result['indicators'] ); - $this->assertSame( '', $result['calculated_at'] ); + $this->assertSame( '', $result['rating'] ); } /** - * Returns score, level, and calculated_at from stored post meta. + * Returns the stored MQA structure verbatim. */ - public function test_get_returns_stored_values(): void { + public function test_get_returns_stored_mqa(): void { $this->load_class(); - \WP_Mock::userFunction( 'get_post_meta' ) - ->with( 42, '_odw_quality_level', true ) - ->andReturn( 'high' ); - - \WP_Mock::userFunction( 'get_post_meta' ) - ->with( 42, '_odw_quality_score', true ) - ->andReturn( '85' ); - - \WP_Mock::userFunction( 'get_post_meta' ) - ->with( 42, '_odw_quality_indicators', true ) - ->andReturn( array( 'title' => array( 'passed' => true ) ) ); + $stored = array( + 'achieved' => 200, + 'assessable' => 230, + 'max' => 405, + 'rating' => 'good', + 'dimensions' => array(), + 'metrics' => array(), + 'calculated_at' => '2026-04-21 10:00:00', + 'score' => 87, + 'level' => 'high', + ); \WP_Mock::userFunction( 'get_post_meta' ) - ->with( 42, '_odw_quality_calculated_at', true ) - ->andReturn( '2026-04-21 10:00:00' ); + ->with( 42, '_odw_mqa', true ) + ->andReturn( $stored ); $result = ODW_Quality::get( 42 ); - $this->assertSame( 85, $result['score'] ); - $this->assertSame( 'high', $result['level'] ); - $this->assertSame( '2026-04-21 10:00:00', $result['calculated_at'] ); + $this->assertSame( 'good', $result['rating'] ); + $this->assertSame( 200, $result['achieved'] ); + $this->assertSame( 87, $result['score'] ); } - // ------------------------------------------------------------------------- - // store() - // ------------------------------------------------------------------------- - /** - * Calls update_post_meta for all four quality meta keys. + * Persists the full MQA array plus the backward-compatible scalar keys. */ - public function test_store_calls_update_post_meta_for_all_keys(): void { + public function test_store_persists_mqa_and_bc_keys(): void { $this->load_class(); $result = array( - 'score' => 75, - 'level' => 'medium', - 'indicators' => array(), + 'rating' => 'good', 'calculated_at' => '2026-04-21 12:00:00', + 'score' => 75, + 'level' => 'high', ); - \WP_Mock::userFunction( 'update_post_meta' ) - ->with( 7, '_odw_quality_score', 75 ) - ->once(); - - \WP_Mock::userFunction( 'update_post_meta' ) - ->with( 7, '_odw_quality_level', 'medium' ) - ->once(); - - \WP_Mock::userFunction( 'update_post_meta' ) - ->with( 7, '_odw_quality_indicators', array() ) - ->once(); - - \WP_Mock::userFunction( 'update_post_meta' ) - ->with( 7, '_odw_quality_calculated_at', '2026-04-21 12:00:00' ) - ->once(); + \WP_Mock::userFunction( 'update_post_meta' )->with( 7, '_odw_mqa', $result )->once(); + \WP_Mock::userFunction( 'update_post_meta' )->with( 7, '_odw_quality_score', 75 )->once(); + \WP_Mock::userFunction( 'update_post_meta' )->with( 7, '_odw_quality_level', 'high' )->once(); + \WP_Mock::userFunction( 'update_post_meta' )->with( 7, '_odw_quality_calculated_at', '2026-04-21 12:00:00' )->once(); ODW_Quality::store( 7, $result ); - // WP_Mock ->once() expectations verified in tearDown; count them explicitly. $this->addToAssertionCount( 4 ); } @@ -281,44 +352,50 @@ public function test_store_calls_update_post_meta_for_all_keys(): void { // ------------------------------------------------------------------------- /** - * Adds odw:qualityScore to the dataset array when a quality level is stored. + * Adds odw:qualityScore (405-scale, rating, dimensions) when MQA data exists. */ - public function test_append_to_jsonld_adds_quality_data(): void { + public function test_append_to_jsonld_adds_mqa_data(): void { $this->load_class(); + \WP_Mock::userFunction( '__' )->andReturnArg( 0 ); \WP_Mock::userFunction( 'get_post_meta' ) - ->with( 5, '_odw_quality_level', true ) - ->andReturn( 'high' ); - - \WP_Mock::userFunction( 'get_post_meta' ) - ->with( 5, '_odw_quality_score', true ) - ->andReturn( '90' ); - - \WP_Mock::userFunction( 'get_post_meta' ) - ->with( 5, '_odw_quality_indicators', true ) - ->andReturn( array() ); - - \WP_Mock::userFunction( 'get_post_meta' ) - ->with( 5, '_odw_quality_calculated_at', true ) - ->andReturn( '2026-04-21 09:00:00' ); + ->with( 5, '_odw_mqa', true ) + ->andReturn( + array( + 'achieved' => 200, + 'assessable' => 230, + 'max' => 405, + 'rating' => 'good', + 'dimensions' => array( + 'findability' => array( + 'achieved' => 100, + 'assessable' => 100, + 'max' => 100, + ), + ), + 'metrics' => array(), + 'calculated_at' => '2026-04-21 09:00:00', + ) + ); $dataset = array( '@type' => 'dcat:Dataset' ); $result = ODW_Quality::append_to_jsonld( $dataset, 5 ); $this->assertArrayHasKey( 'odw:qualityScore', $result ); - $this->assertSame( 90, $result['odw:qualityScore']['odw:score'] ); - $this->assertSame( 'high', $result['odw:qualityScore']['odw:level'] ); - $this->assertSame( 100, $result['odw:qualityScore']['odw:maxScore'] ); + $this->assertSame( 200, $result['odw:qualityScore']['odw:score'] ); + $this->assertSame( 'good', $result['odw:qualityScore']['odw:rating'] ); + $this->assertSame( 405, $result['odw:qualityScore']['odw:maxScore'] ); + $this->assertArrayHasKey( 'findability', $result['odw:qualityScore']['odw:dimensions'] ); } /** - * Leaves the dataset array unchanged when no quality level is stored. + * Leaves the dataset array unchanged when no MQA data is stored. */ - public function test_append_to_jsonld_skips_when_no_level(): void { + public function test_append_to_jsonld_skips_when_no_data(): void { $this->load_class(); \WP_Mock::userFunction( 'get_post_meta' ) - ->with( 5, '_odw_quality_level', true ) + ->with( 5, '_odw_mqa', true ) ->andReturn( '' ); $dataset = array( '@type' => 'dcat:Dataset' );
' . esc_html__( 'Pflichtfelder', 'open-data-wizard' ) . '
' . esc_html( $section ) . '
' + . esc_html( $dim_labels[ $dim ] ?? $dim ) + . ' ' . esc_html( sprintf( '%d / %d', $d_ach, $d_ass ) ) + . ( $d_ass !== $d_max ? esc_html( sprintf( ' (max. %d)', $d_max ) ) : '' ) + . '
' . esc_html__( 'Optionale Angaben', 'open-data-wizard' ) . '