From 91433fee3305bd02523ceaa7f2dc024f56884869 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 12:12:17 +0000 Subject: [PATCH 1/2] fix: Carbon-Fields-Bedienelemente auf Deutsch (v2.47.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Im Formular standen auch in einem deutschen WordPress englische Texte aus der Oberfläche von Carbon Fields (Domain carbon-fields-ui): „There are no entries yet.", „Add Distribution", „Select Date", „No options." u. a. Ursachen: - Carbon Fields 3.6.11 bringt für carbon-fields-ui keine de_DE-Datei mit. - Carbon Fields reicht seine UI-Übersetzungen über wp_localize_script() als cf.config.locale an setLocaleData() weiter und liest sie über get_translations_for_domain()->entries. Seit WP 6.5 ist das eine Liste; Carbon Fields nimmt die Indizes als msgids, keine Übersetzung kommt an. Änderungen: - ODW_Carbon_Fields_L10n: lädt languages/carbon-fields-ui-de_DE.mo für alle de*-Locales und baut cf.config.locale im Filter carbon_fields_config nach msgid (samt Kontext) geschlüsselt neu auf. - languages/carbon-fields-ui-de_DE.po/.mo (Anrede „Sie"), nicht in vendor/. - assets/js/odw-carbon-fields-l10n.js: feste react-select-Texte der Mehrfachauswahl („Select...", „No options") über die Plugin-Domain. - odw-datepicker-de.js: wirkungslosen Platzhalter-Tausch entfernt. - en_US-.po/.mo und .pot um die zwei neuen Zeichenketten ergänzt. - Unit-Tests und E2E-Zusicherung im deutschen Formular. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GcBuax4aJvDgWhT9oi6vNM --- CHANGELOG.md | 38 +++ CLAUDE.md | 26 +- README.md | 2 +- assets/js/odw-carbon-fields-l10n.js | 59 ++++ assets/js/odw-datepicker-de.js | 11 +- includes/class-admin.php | 15 + includes/class-carbon-fields-l10n.php | 241 ++++++++++++++++ languages/carbon-fields-ui-de_DE.mo | Bin 0 -> 3446 bytes languages/carbon-fields-ui-de_DE.po | 190 +++++++++++++ languages/open-data-wizard-en_US.mo | Bin 80555 -> 80647 bytes languages/open-data-wizard-en_US.po | 6 + languages/open-data-wizard.pot | 6 + open-data-wizard.php | 6 +- package-lock.json | 4 +- package.json | 2 +- tests/e2e/admin-workflow.spec.js | 32 +++ tests/test-carbon-fields-l10n.php | 378 ++++++++++++++++++++++++++ 17 files changed, 1001 insertions(+), 15 deletions(-) create mode 100644 assets/js/odw-carbon-fields-l10n.js create mode 100644 includes/class-carbon-fields-l10n.php create mode 100644 languages/carbon-fields-ui-de_DE.mo create mode 100644 languages/carbon-fields-ui-de_DE.po create mode 100644 tests/test-carbon-fields-l10n.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ebfa138..4b8780f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,44 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.ht --- +## [2.47.3] — 2026-11-06 + +### 🐛 Fixed +- **Carbon-Fields-Bedienelemente auf Deutsch.** Im Formular standen auch in einem deutschen + WordPress englische Texte von Carbon Fields: „There are no entries yet.“ und + „Add Distribution“ im Wiederholungsfeld „Weitere Distributionen“ (ebenso bei den + Übersetzungs-Wiederholungsfeldern in Tab 2), „Select Date“ am Datumsfeld, „No options.“, + „Collapse All“ u. a. Zwei Ursachen: + - Carbon Fields 3.6.11 bringt für seine Oberflächen-Domain `carbon-fields-ui` keine deutsche + Datei mit. Das Plugin liefert sie jetzt selbst aus + (`languages/carbon-fields-ui-de_DE.po`/`.mo`, Anrede „Sie“), nicht in `vendor/`. + - Carbon Fields gibt seine UI-Übersetzungen nicht über `wp_set_script_translations()` weiter, + sondern über `wp_localize_script()` als `cf.config.locale`, das die Oberfläche an + `setLocaleData()` übergibt. Seit WordPress 6.5 liefert + `get_translations_for_domain()->entries` dafür eine Liste statt einer nach msgid + geschlüsselten Tabelle; Carbon Fields übernimmt die Listenindizes als msgids, und keine + Übersetzung erreicht die Oberfläche — auch die mitgelieferten (z. B. fr_FR) nicht. Die neue + Klasse `ODW_Carbon_Fields_L10n` baut die Daten im Filter `carbon_fields_config` korrekt + geschlüsselt neu auf und lädt für alle deutschen Locales (de_DE, de_DE_formal, de_AT, de_CH …) + die gebündelte Datei. +- **Mehrfachauswahl (Themen, Engagementfelder):** Platzhalter „Select...“ und die Meldung + „No options“ im Aufklappmenü sind feste Vorgaben der Bibliothek react-select und laufen durch + keine Textdomain. `assets/js/odw-carbon-fields-l10n.js` ersetzt sie durch „Auswählen…“ und + „Keine passenden Optionen“ (über die Plugin-Domain; en_US erhält das englische Original). +- **„Datum wählen“ statt „Select Date“:** Der Button am Datumsfeld kommt jetzt regulär aus der + Übersetzung. Der bisherige Tausch in `odw-datepicker-de.js` suchte „Select Date“ als + Platzhalter des Eingabefelds — es ist aber die Beschriftung eines Buttons, der Tausch blieb + deshalb wirkungslos und ist entfallen. + +### 🧪 Tests +- `tests/test-carbon-fields-l10n.php`: Schlüsselbildung mit WP_Translations (nur `__get()`, + Liste) und älteren Übersetzungsobjekten, Laden nur bei deutscher Locale, gebündelte `.mo` + passend zur `.po`. +- E2E (`admin-workflow.spec.js`): Das deutsche Formular zeigt „Noch keine Einträge vorhanden.“, + „Distribution hinzufügen“ und „Datum wählen“ und keinen der englischen Texte. + +--- + ## [2.47.2] — 2026-11-03 ### 🐛 Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 8137a711..1de540c1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -92,6 +92,7 @@ open-data-wizard/ │ ├── class-samples.php # Beispieldaten laden/entfernen (_odw_sample), nie im Endpunkt │ ├── class-post-types.php # CPT: odw_dataset │ ├── class-fields.php # Carbon Fields Formular (5 Tabs) +│ ├── class-carbon-fields-l10n.php # Deutsche UI-Texte für Carbon Fields (Filter carbon_fields_config) │ ├── class-validation.php # Publishing-Validation │ ├── class-quality.php # Qualitäts-Scoring (0-100) │ ├── class-admin.php # Admin UI, Spalten, Intro-Seite @@ -113,7 +114,8 @@ open-data-wizard/ │ │ ├── odw-catalog.js # Katalog: Schlagwort-Aufklappfeld, Einklappen auf dem Handy │ │ ├── wizard-tabs.js # Tab-Navigation mit SessionStorage │ │ ├── odw-admin-fields.js # Field-Initialization, Autosuggest -│ │ └── odw-file-upload.js # File-Upload Handler (wp.media) +│ │ ├── odw-file-upload.js # File-Upload Handler (wp.media) +│ │ └── odw-carbon-fields-l10n.js # Feste react-select-Texte der Mehrfachauswahl („Select...") │ └── images/ # Icons, Logos ├── config/ │ ├── phpcs.xml # WordPress Coding Standards @@ -145,6 +147,7 @@ open-data-wizard/ │ ├── test-rest-delta.php # REST: Delta, Content Negotiation, Ausschlüsse (Beispiele, Passwort) │ ├── test-batch-import.php # Batch-Import: Parsing, Spalten, Vokabulare, Beispieldateien │ ├── test-samples.php # Beispieldaten: Laden/Entfernen/Übernehmen, Demo-Migration +│ ├── test-carbon-fields-l10n.php # Carbon-Fields-UI auf Deutsch: Locale-Daten, gebündelte .mo │ ├── test-cli.php # CLI Commands Tests │ ├── test-validation.php, test-rdf.php, test-vocabulary.php, test-registry-schema.php, │ │ test-field-catalog.php, test-shacl-shapes.php @@ -155,7 +158,8 @@ open-data-wizard/ ├── languages/ │ ├── open-data-wizard.pot # i18n Translation Template │ ├── open-data-wizard-en_US.po # Englische Übersetzung (Quelle) -│ └── open-data-wizard-en_US.mo # Englische Übersetzung (kompiliert) +│ ├── open-data-wizard-en_US.mo # Englische Übersetzung (kompiliert) +│ └── carbon-fields-ui-de_DE.po/.mo # Deutsche Texte der Carbon-Fields-Oberfläche ├── README.md # User-facing Dokumentation (DE) + Doku-Navigation ├── DOCUMENTATION.md # Technische Dokumentation (Architektur, REST-API, CLI …) ├── TECHNICAL-SPEC.md # Technische Spezifikationen + DCAT-AP-Roadmap/Umsetzungsstand @@ -194,6 +198,7 @@ Externe Harvester rufen /catalog, /datasets/, oder /delta ab | **ODW_Samples** | Beispieldaten aus `samples/import-example.json` laden/entfernen/übernehmen; Kennzeichen `_odw_sample` hält sie aus `/catalog`, `/datasets`, `/delta` heraus und im Frontend-Katalog nur für die Redaktion sichtbar (oder `show_demo="1"`) | `load()`, `remove()`, `adopt()`, `is_sample()`, `exclude_clause()` | | **ODW_Post_Types** | CPT Registrierung mit Capabilities | `register()` | | **ODW_Fields** | Carbon Fields Formular (5 Tabs) + JSON-LD Builder | `register_required_fields()`, `odw_build_dataset_jsonld()` | +| **ODW_Carbon_Fields_L10n** | Deutsche Texte der Carbon-Fields-Oberfläche (Domain `carbon-fields-ui`): lädt `languages/carbon-fields-ui-de_DE.mo` für alle `de*`-Locales und baut `cf.config.locale` nach msgid geschlüsselt neu auf (Carbon Fields scheitert daran seit WP 6.5) | `filter_config()`, `locale_data()`, `is_german_locale()` | | **ODW_Validation** | Publish-Blocking bei fehlenden Pflichtfeldern | `intercept_publish()`, `validate()` | | **ODW_Quality** | Qualitäts-Scoring & Caching | `calculate()`, `get_level()` | | **ODW_Admin** | Admin UI (Spalten, Intro-Seite, Datei-Upload-Widget in Tab 3) | `register_introduction_page()`, `render_column()`, `file_upload_html()`, `save_file_attachment()` | @@ -454,6 +459,17 @@ esc_attr__( 'Attribute Text', 'open-data-wizard' ) - **English**: `languages/open-data-wizard-en_US.po` / `.mo` — overrides the German source strings on `en_US` installs (rund 750 Einträge) - Loaded via `load_plugin_textdomain()` on `init` hook + +**Carbon-Fields-Oberfläche (`carbon-fields-ui`).** Bedienelemente wie „Add %s", „There are no +entries yet.", „Select Date" stammen aus Carbon Fields, nicht aus dem Plugin. Carbon Fields +bringt dafür kein Deutsch mit und reicht seine Übersetzungen nicht über +`wp_set_script_translations()`, sondern über den Filter `carbon_fields_config` +(`cf.config.locale` → `setLocaleData()`) an die Oberfläche. `ODW_Carbon_Fields_L10n` setzt dort +an; die Übersetzung liegt in `languages/carbon-fields-ui-de_DE.po` (nach Änderungen mit +`bin/compile-mo.py` neu kompilieren), **nie in `vendor/`**. Platzhalter „Select..." und +„No options" der Mehrfachauswahl sind feste react-select-Texte und werden von +`assets/js/odw-carbon-fields-l10n.js` getauscht. Nach einem Carbon-Fields-Update prüfen, ob neue +`__( '…', 'carbon-fields-ui' )`-Aufrufe dazugekommen sind. - The `.mo` is generated without gettext tooling; if strings change, regenerate the `.po`/`.mo` (see the generator approach in the v2.3.0 PR) @@ -828,7 +844,7 @@ Update **both** locations: - MINOR: New features (backward-compatible) - PATCH: Bug fixes only -Current: **v2.47.2** +Current: **v2.47.3** --- @@ -882,6 +898,8 @@ Neue technische Festlegungen gehören dorthin, nicht in README oder CLAUDE.md. - **Beispieldaten statt Demo-Datensatz (v2.47.0):** leerer Start, Laden/Entfernen per Klick oder WP-CLI, nie in einem Harvest-Endpunkt, im Frontend nur für die Redaktion. - **Nur öffentliche Datensätze in den Endpunkten, CI mit Leserechten (v2.47.1).** +- **Carbon-Fields-Bedienelemente auf Deutsch (v2.47.3):** gebündelte `carbon-fields-ui`-Übersetzung + und korrekt geschlüsselte Locale-Daten (siehe Internationalisierung). ### ☐ Noch offen / geplant @@ -981,6 +999,6 @@ Neue technische Festlegungen gehören dorthin, nicht in README oder CLAUDE.md. --- -**Zuletzt aktualisiert**: Version 2.47.2 (November 2026) +**Zuletzt aktualisiert**: Version 2.47.3 (November 2026) **Autor**: Open Data Wizard Team (nozilla) **License**: GPL-2.0-or-later diff --git a/README.md b/README.md index 310a8add..fec646b3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

Lizenz - Version + Version DCAT-AP 3.0 PHP >= 8.1 WordPress 6.4+ diff --git a/assets/js/odw-carbon-fields-l10n.js b/assets/js/odw-carbon-fields-l10n.js new file mode 100644 index 00000000..3f4703e2 --- /dev/null +++ b/assets/js/odw-carbon-fields-l10n.js @@ -0,0 +1,59 @@ +/* global odwCarbonFieldsL10n */ +/** + * Open Data Wizard — Texte der Mehrfachauswahl, die keine Übersetzung erreicht. + * + * Die Mehrfachauswahl von Carbon Fields (Themen, Engagementfelder) baut auf + * react-select auf. Dessen Platzhalter „Select..." und die Meldung + * „No options" im Aufklappmenü sind Vorgaben der Bibliothek; Carbon Fields + * reicht dafür keine Eigenschaft durch, und sie laufen durch keine Textdomain. + * Alle übrigen Carbon-Fields-Texte kommen regulär über die Übersetzung + * (siehe includes/class-carbon-fields-l10n.php) und werden hier nicht angefasst. + * + * Getauscht wird nur Text, der exakt dem englischen Original entspricht, und + * nur in den Elementen mit dem Klassenpräfix `cf-multiselect`, das Carbon + * Fields an react-select übergibt. React schreibt diesen Text nicht erneut, + * solange er sich nicht ändert; erscheint das Element neu (Menü öffnen, + * Auswahl leeren), fängt der MutationObserver es wieder ab. + */ +( function () { + 'use strict'; + + var map = ( typeof odwCarbonFieldsL10n !== 'undefined' && odwCarbonFieldsL10n.reactSelect ) || {}; + + var SELECTOR = '.cf-multiselect__placeholder, .cf-multiselect__menu-notice'; + + function localize( el ) { + var text = el.textContent; + if ( Object.prototype.hasOwnProperty.call( map, text ) && map[ text ] && map[ text ] !== text ) { + el.textContent = map[ text ]; + } + } + + function scan( root ) { + if ( ! root || root.nodeType !== 1 ) { + return; + } + if ( root.matches( SELECTOR ) ) { + localize( root ); + } + root.querySelectorAll( SELECTOR ).forEach( localize ); + } + + function start() { + if ( Object.keys( map ).length === 0 ) { + return; + } + scan( document.body ); + new MutationObserver( function ( mutations ) { + mutations.forEach( function ( mutation ) { + mutation.addedNodes.forEach( scan ); + } ); + } ).observe( document.body, { childList: true, subtree: true } ); + } + + if ( document.readyState === 'loading' ) { + document.addEventListener( 'DOMContentLoaded', start ); + } else { + start(); + } +} )(); diff --git a/assets/js/odw-datepicker-de.js b/assets/js/odw-datepicker-de.js index 63509930..5651a7ee 100644 --- a/assets/js/odw-datepicker-de.js +++ b/assets/js/odw-datepicker-de.js @@ -2,8 +2,12 @@ * Open Data Wizard — German localization for the Carbon Fields date picker (flatpickr). * * Registers the German locale on the global flatpickr instance and re-localizes - * any pickers that were already initialised, so month names, weekday names and - * the "Select Date" placeholder appear in German. + * any pickers that were already initialised, so month and weekday names appear + * in German. + * + * The "Select Date" button is not handled here: it is a regular Carbon Fields + * UI string and comes from languages/carbon-fields-ui-de_DE.mo via + * ODW_Carbon_Fields_L10n. */ ( function () { 'use strict'; @@ -57,9 +61,6 @@ if ( input._flatpickr ) { input._flatpickr.set( 'locale', German ); } - if ( input.placeholder === 'Select Date' || input.placeholder === 'Select date' ) { - input.placeholder = 'Datum wählen…'; - } } ); } diff --git a/includes/class-admin.php b/includes/class-admin.php index cebac6ad..c5fd9aa0 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -461,6 +461,21 @@ public static function enqueue_assets( string $hook ): void { true ); + // Platzhalter und Leermeldung der Mehrfachauswahl (react-select). + // Die übrigen Carbon-Fields-Texte kommen über ODW_Carbon_Fields_L10n. + wp_enqueue_script( + 'odw-carbon-fields-l10n', + ODW_PLUGIN_URL . 'assets/js/odw-carbon-fields-l10n.js', + array(), + ODW_VERSION, + true + ); + wp_localize_script( + 'odw-carbon-fields-l10n', + 'odwCarbonFieldsL10n', + array( 'reactSelect' => ODW_Carbon_Fields_L10n::react_select_strings() ) + ); + global $post; $file_id = $post ? (int) get_post_meta( $post->ID, '_odw_file_id', true ) : 0; $file_name = ''; diff --git a/includes/class-carbon-fields-l10n.php b/includes/class-carbon-fields-l10n.php new file mode 100644 index 00000000..3663159a --- /dev/null +++ b/includes/class-carbon-fields-l10n.php @@ -0,0 +1,241 @@ +entries aus und gibt sie per + * wp_localize_script() als `cf.config.locale` an die Oberfläche, die damit + * `setLocaleData()` aus @wordpress/i18n aufruft. Seit WordPress 6.5 liefert + * `->entries` aber eine Liste (WP_Translations::__get()), keine nach msgid + * geschlüsselte Tabelle. Carbon Fields übernimmt die Listenindizes 0, 1, 2 … + * als msgids — die Übersetzungen kommen in der Oberfläche nie an, auch die + * mitgelieferten nicht. + * + * Diese Klasse behebt beides an der Stelle, die Carbon Fields dafür vorsieht, + * dem Filter `carbon_fields_config`: Für deutsche Locales lädt sie die + * gebündelte Übersetzung `languages/carbon-fields-ui-de_DE.mo` in die Domain + * `carbon-fields-ui` und baut `cf.config.locale` danach neu auf, geschlüsselt + * nach msgid (samt Kontext). Nichts davon liegt in `vendor/`, das nicht + * eingecheckt ist und bei jedem `composer install` neu entsteht. + * + * Die Mehrfachauswahl zeigt zusätzlich zwei Texte, die keine Übersetzung + * erreichen kann: „Select..." und „No options" setzt die Bibliothek + * react-select als feste Vorgabe, Carbon Fields reicht dafür nichts durch. + * Diese beiden ersetzt assets/js/odw-carbon-fields-l10n.js im DOM; die + * Zielwörter kommen aus react_select_strings() und laufen damit durch die + * Textdomain des Plugins (en_US erhält wieder das englische Original). + * + * @package OpenDataWizard + */ + +declare(strict_types=1); + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * Supplies German UI strings to the Carbon Fields JavaScript interface. + * + * @package OpenDataWizard + */ +class ODW_Carbon_Fields_L10n { + + /** Textdomain der Carbon-Fields-Oberfläche. */ + public const UI_DOMAIN = 'carbon-fields-ui'; + + /** + * Ob die deutsche Datei in diesem Request bereits geladen wurde. + * + * @var bool + */ + private static bool $german_loaded = false; + + /** + * Registers hooks. + */ + public static function init(): void { + add_filter( 'carbon_fields_config', array( self::class, 'filter_config' ) ); + } + + /** + * Ob eine Locale deutsch ist (de_DE, de_DE_formal, de_AT, de_CH, …). + * + * Alle deutschen Varianten erhalten dieselbe Datei: Das Plugin spricht + * durchgehend mit „Sie" an, eine eigene du-Fassung gibt es nicht. + * + * @param string|null $locale Locale; ohne Angabe die des aktuellen Requests. + */ + public static function is_german_locale( ?string $locale = null ): bool { + $locale = $locale ?? determine_locale(); + + return 'de' === $locale || 0 === strpos( $locale, 'de_' ); + } + + /** + * Absoluter Pfad der gebündelten deutschen Übersetzung. + */ + public static function german_mo_path(): string { + return ODW_PLUGIN_DIR . 'languages/carbon-fields-ui-de_DE.mo'; + } + + /** + * Filter `carbon_fields_config`: lädt bei deutscher Locale die gebündelte + * Übersetzung und baut `config.locale` nach msgid geschlüsselt neu auf. + * + * Carbon Fields ruft den Filter in Loader::enqueue_assets() auf + * (`admin_print_footer_scripts`), also nur auf Seiten mit seinen Feldern + * und erst, wenn die Locale feststeht. + * + * @param mixed $config Konfiguration, die Carbon Fields als `window.cf` ausgibt. + * @return mixed + */ + public static function filter_config( $config ) { + if ( ! is_array( $config ) || ! isset( $config['config'] ) || ! is_array( $config['config'] ) ) { + return $config; + } + + if ( self::is_german_locale() ) { + self::load_german(); + } + + $header = array(); + if ( isset( $config['config']['locale'][''] ) && is_array( $config['config']['locale'][''] ) ) { + $header = $config['config']['locale']['']; + } + + $config['config']['locale'] = self::locale_data( get_translations_for_domain( self::UI_DOMAIN ), $header ); + + return $config; + } + + /** + * Lädt die deutsche Übersetzung in die Domain `carbon-fields-ui`. + * + * Ergänzt eine bereits geladene Datei, statt sie zu ersetzen — sollte + * Carbon Fields künftig selbst eine deutsche Datei mitbringen, bleiben + * deren Einträge erhalten, wo diese Datei keine hat. + */ + public static function load_german(): bool { + if ( self::$german_loaded ) { + return true; + } + + $path = self::german_mo_path(); + if ( ! is_readable( $path ) ) { + return false; + } + + self::$german_loaded = (bool) load_textdomain( self::UI_DOMAIN, $path ); + + return self::$german_loaded; + } + + /** + * Baut die Locale-Daten für `setLocaleData()` (@wordpress/i18n). + * + * Format: `'' => [ domain, lang, plural_forms ]`, danach je Eintrag + * `msgid => [ Übersetzung, … ]`; mit Kontext lautet der Schlüssel + * `Kontext\u0004msgid`. Der Schlüssel wird aus `singular`/`context` des + * Eintrags gebildet und nicht aus dem Array-Index — das ist der Punkt, an + * dem Carbon Fields seit WordPress 6.5 scheitert. So funktioniert es mit + * der Liste aus WP_Translations ebenso wie mit der nach Schlüssel + * indizierten Tabelle älterer Versionen. + * + * @param object|null $translations Ergebnis von get_translations_for_domain(). + * @param array $header Kopfeintrag, den Carbon Fields gebildet hat. + * @return array + */ + public static function locale_data( $translations, array $header = array() ): array { + $header = array_merge( + array( + 'domain' => self::UI_DOMAIN, + 'lang' => determine_locale(), + ), + $header + ); + + $headers = self::read_array( $translations, 'headers' ); + if ( empty( $header['plural_forms'] ) && ! empty( $headers['Plural-Forms'] ) ) { + $header['plural_forms'] = (string) $headers['Plural-Forms']; + } + + $data = array( '' => $header ); + + $entries = self::read_array( $translations, 'entries' ); + + foreach ( $entries as $entry ) { + if ( ! is_object( $entry ) || ! isset( $entry->singular ) || '' === (string) $entry->singular ) { + continue; + } + + $values = isset( $entry->translations ) && is_array( $entry->translations ) + ? array_map( 'strval', array_values( $entry->translations ) ) + : array(); + if ( '' === implode( '', $values ) ) { + continue; + } + + $key = (string) $entry->singular; + if ( isset( $entry->context ) && '' !== (string) $entry->context ) { + $key = $entry->context . "\4" . $key; + } + + $data[ $key ] = $values; + } + + return $data; + } + + /** + * Liest eine Array-Eigenschaft eines Übersetzungsobjekts. + * + * Nicht über isset() oder `??`: WP_Translations (ab WordPress 6.5) stellt + * `entries` und `headers` nur über __get() bereit und kennt kein + * __isset() — isset() wäre dort immer false, die Übersetzungen fielen + * stillschweigend weg. NOOP_Translations und MO haben echte Eigenschaften. + * + * @param mixed $source Übersetzungsobjekt. + * @param string $name Eigenschaft. + * @return array + */ + private static function read_array( $source, string $name ): array { + if ( ! is_object( $source ) ) { + return array(); + } + if ( ! property_exists( $source, $name ) && ! method_exists( $source, '__get' ) ) { + return array(); + } + + $value = $source->$name; + + return is_array( $value ) ? $value : array(); + } + + /** + * Texte für die Mehrfachauswahl, die react-select fest vorgibt. + * + * Schlüssel: englischer Originaltext im DOM; Wert: Ersatz. Auf en_US + * übersetzt die Plugin-Domain zurück ins Original — dann ist der Tausch + * ein No-op. + * + * @return array + */ + public static function react_select_strings(): array { + return array( + 'Select...' => __( 'Auswählen…', 'open-data-wizard' ), + 'No options' => __( 'Keine passenden Optionen', 'open-data-wizard' ), + ); + } +} diff --git a/languages/carbon-fields-ui-de_DE.mo b/languages/carbon-fields-ui-de_DE.mo new file mode 100644 index 0000000000000000000000000000000000000000..3dcc0fef5d7bae1d2df396423b7dec25dd3026de GIT binary patch literal 3446 zcmb7`O>87b6@Uv7eqzExf&>BrRK%LZL^Er9VH0L|L+tfb-jJ)$9551NVGK@pqcfgM5DWHj$=(Yj0PIf3?onJK@vtes}|Z7T$#C;0sXp z_!E2#-hYo$?|_%!d*PLd{3ZBqArqvr0{u9c+_Yz!L_c17Oe-w&8=O$d6JpUq;bKZnv?<-L9 z^WBO3Qz&`)HGByE7D|5p1~oi^5r*m%lzKP^#m*Hd{yqmug>q0WWtn{RY*0&18}AFR zi!?30$(sK$!;~KNZEke*yVmt|EOhEk5}D{59c*Sgi9e23*Tl51lMss3Ceap77ehff zzH%2og3gkd9ABp}gAQjHdb4BAsHraGCh>gRbX(41G8>VKVAO>o&y(SifnLgUwNegQ z%Xw8>mbR;4CAsnSI4_b+0@KNhbh{-nZQmLuE~~Uxt;sW8bamVv8vXEWcv#z@T@iIC zhr_~0T(eGJETeH9<9kh2X#{oA6lsweJxc23X=*~~mbvbdEs1>>e~XcrCLf`2Ev=p+ z9raPwdUDGS%7Jzz5g@@`ZKD|kS)KN=`^an<&&;WH>;CIfar9WreK8vQIbFSQk^);i z8RpdGVw!5k-h?ub0sl)!y4>{X)_)k&KHtFXwg1z*M1>u^H91*HTx_ zpx7~Llf=xYJA`^_MG`rAyh=6I<4c32cPMPx!QnX3X^|IRZT5?i zWK>@n7wvdrjg^2?L=g5?Ot6wPkKawZ$=#EF&5L8zp5T(e6lItGMvo zvx1MKOkMPr+TS&?i6%AFttyLwH_w;0y183@GVKzZt80Yu=;=kSjw^%lS+CnrCb`=1 z#Z6+>xRf=n8y~ojEe>RyCQ^xh#_lCP(~Hf|Jk~rtbJa-2Hr7MWW{u^t7aChdOV4av zXy?DLr8D!~N_%Gg(t3MpbfGyv zbD<#FE;hEfEG-?0(y3vdSa+71NPIBy{I#vs#^cj9$O7rv2QSgCA5YwrRE$n&MI!y;s&^)o=GKweJXHy@eV_ zc|bSCrHRC4A;X1AUSCOm``Vo*`g4r7k`B zdL2GicXC0NUctsZs$h26dB@OY$_+DIoH!wf6h#ZiF>og*ym&GSsKZ~ zZ7It)DNY;q7pQA9>1(W{0Q$Vpq_D?N+P-Hx&IX!E&vZGpJi1)@UCV0OMoN8jaGTpf zCMyXow%a6>Qs0;9=i}SqOeyn6?+6~WRNF;vw?lukX{asU7T literal 0 HcmV?d00001 diff --git a/languages/carbon-fields-ui-de_DE.po b/languages/carbon-fields-ui-de_DE.po new file mode 100644 index 00000000..84a48fd2 --- /dev/null +++ b/languages/carbon-fields-ui-de_DE.po @@ -0,0 +1,190 @@ +# Open Data Wizard — deutsche Übersetzung der Carbon-Fields-Oberfläche. +# +# Textdomain: carbon-fields-ui (JavaScript-Oberfläche von Carbon Fields 3.6). +# Carbon Fields bringt für diese Domain keine deutsche Datei mit; ohne sie +# zeigt das Formular auch in einem deutschen WordPress „There are no entries +# yet.", „Add Distribution" oder „Select Date". Geladen wird die Datei von +# ODW_Carbon_Fields_L10n für alle deutschen Locales (de_DE, de_DE_formal, +# de_AT, de_CH …) — Anrede „Sie", wie im übrigen Plugin. +# +# Die msgids stammen aus dem Quelltext von Carbon Fields 3.6.11 +# (packages/**/*.js, Aufrufe von __( '…', 'carbon-fields-ui' )). Meldungen, +# die nur in der Browser-Konsole für Entwickler:innen erscheinen (Registry, +# Container-Bedingungen), bleiben bewusst englisch. +# +# Nach Änderungen neu kompilieren: +# python3 bin/compile-mo.py languages/carbon-fields-ui-de_DE.po languages/carbon-fields-ui-de_DE.mo +# +# This file is distributed under the same license as the plugin. +msgid "" +msgstr "" +"Project-Id-Version: Open Data Wizard 2.47.3\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2026-11-06 00:00+0000\n" +"Language: de_DE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Domain: carbon-fields-ui\n" + +# Wiederholungsfeld (complex) +msgid "There are no entries yet." +msgstr "Noch keine Einträge vorhanden." + +msgid "Add %s" +msgstr "%s hinzufügen" + +msgid "Expand All" +msgstr "Alle aufklappen" + +msgid "Collapse All" +msgstr "Alle zuklappen" + +msgid "Collapse" +msgstr "Zuklappen" + +msgid "Duplicate" +msgstr "Duplizieren" + +msgid "Remove" +msgstr "Entfernen" + +msgid "This field is required." +msgstr "Dieses Feld ist ein Pflichtfeld." + +msgid "Minimum number of rows not reached (%1$d %2$s)" +msgstr "Mindestanzahl an Einträgen nicht erreicht (%1$d %2$s)" + +# Datum und Uhrzeit +msgid "Select Date" +msgstr "Datum wählen" + +msgid "Select Time" +msgstr "Uhrzeit wählen" + +# Auswahlfelder (multiselect, set, select) +msgid "No options." +msgstr "Keine Optionen vorhanden." + +msgid "Show All Options" +msgstr "Alle Optionen anzeigen" + +msgid "Show Less Options" +msgstr "Weniger Optionen anzeigen" + +msgid "Search..." +msgstr "Suchen…" + +msgid "Please choose" +msgstr "Bitte wählen" + +# Verknüpfungen (association) +msgid "Add" +msgstr "Hinzufügen" + +msgid "Edit" +msgstr "Bearbeiten" + +msgid "Thumbnail" +msgstr "Vorschaubild" + +msgid "Showing %1$d of %2$d results" +msgstr "%1$d von %2$d Ergebnissen" + +msgid "Maximum number of items reached (%s items)" +msgstr "Höchstzahl an Elementen erreicht (%s Elemente)" + +msgid "Minimum number of items not reached (%s items)" +msgstr "Mindestanzahl an Elementen nicht erreicht (%s Elemente)" + +msgid "An error occurred while trying to fetch association options." +msgstr "Beim Laden der Auswahlmöglichkeiten ist ein Fehler aufgetreten." + +# Dateien und Medien +msgid "Select File" +msgstr "Datei auswählen" + +msgid "Use File" +msgstr "Datei verwenden" + +msgid "Select Image" +msgstr "Bild auswählen" + +msgid "Use Image" +msgstr "Bild verwenden" + +msgid "Select Attachments" +msgstr "Anhänge auswählen" + +msgid "Use Attachments" +msgstr "Anhänge verwenden" + +msgid "An error occurred while trying to fetch files data." +msgstr "Beim Laden der Dateiangaben ist ein Fehler aufgetreten." + +# Farbe +msgid "Select a color" +msgstr "Farbe wählen" + +msgid "Clear" +msgstr "Zurücksetzen" + +# Rich-Text +msgid "Visual" +msgstr "Visuell" + +msgid "Text" +msgstr "Text" + +msgid "Click to edit" +msgstr "Zum Bearbeiten klicken" + +msgid "Close" +msgstr "Schließen" + +# oEmbed und Karte +msgid "Not Found" +msgstr "Nicht gefunden" + +msgid "An error occurred while trying to fetch oembed preview." +msgstr "Beim Laden der Einbettungsvorschau ist ein Fehler aufgetreten." + +msgid "The address could not be found." +msgstr "Die Adresse wurde nicht gefunden." + +msgid "Geocode was not successful for the following reason: " +msgstr "Die Geokodierung ist aus folgendem Grund fehlgeschlagen: " + +msgid "Error alert" +msgstr "Fehlermeldung" + +# Seitenleisten +msgid "Please enter the name of the new sidebar:" +msgstr "Bitte geben Sie den Namen der neuen Seitenleiste ein:" + +msgid "An error occurred while trying to create the sidebar." +msgstr "Beim Anlegen der Seitenleiste ist ein Fehler aufgetreten." + +# Allgemein +msgid "An error occured." +msgstr "Es ist ein Fehler aufgetreten." + +# Block-Editor +msgid "Fields" +msgstr "Felder" + +msgid "Show preview" +msgstr "Vorschau anzeigen" + +msgid "Hide preview" +msgstr "Vorschau ausblenden" + +msgid "No results found." +msgstr "Keine Ergebnisse gefunden." + +msgid "Error loading block: %s" +msgstr "Fehler beim Laden des Blocks: %s" + +msgid "Field of type '%s' is not supported in Gutenberg." +msgstr "Feldtyp „%s“ wird im Block-Editor nicht unterstützt." diff --git a/languages/open-data-wizard-en_US.mo b/languages/open-data-wizard-en_US.mo index 673510a64eaa927ab01c4c3a24aa2fb47986f1b9..966387807b312bd64f30857b39f8f8a81ce60785 100644 GIT binary patch delta 12307 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 { await expect(page.locator(cfField('odw_language'))).toBeVisible(); }); + test('Bedienelemente von Carbon Fields sprechen Deutsch', async ({ page }) => { + await page.goto('/wp-admin/post-new.php?post_type=odw_dataset'); + + // Die Texte stammen aus der Oberfläche von Carbon Fields (Domain + // carbon-fields-ui), nicht aus dem Plugin. Sie kommen über + // ODW_Carbon_Fields_L10n und languages/carbon-fields-ui-de_DE.mo an; ohne + // beides stand hier „There are no entries yet." und „Add Distribution". + + // Mehrfachauswahl in Tab 1 (aktiv): Platzhalter von react-select. + const themen = page.locator('.cf-field.cf-multiselect', { hasText: 'Welchen Themen' }); + await expect(themen.locator('.cf-multiselect__placeholder')).toHaveText('Auswählen…'); + + // Wiederholungsfeld in Tab 3: Leerzustand und Hinzufügen-Button. + await page.locator('.cf-container__tabs-item button', { hasText: 'Datenbereitstellung' }).click(); + const weitere = page.locator('.cf-field.cf-complex', { hasText: 'Weitere Distributionen' }); + await expect(weitere.locator('.cf-complex__placeholder-label')).toHaveText('Noch keine Einträge vorhanden.'); + await expect(weitere.locator('.cf-complex__inserter-button')).toHaveText('Distribution hinzufügen'); + + // Datumsfeld: Beschriftung des Kalender-Buttons. Ohne Sichtbarkeitsprüfung, + // weil das Feld in einer aufklappbaren Gruppe stehen kann — es geht um den + // Text, und Carbon Fields rendert alle Reiter ins DOM. + const zeitraum = page.locator('.cf-field', { has: page.locator(cfField('odw_temporal_start')) }); + await expect(zeitraum.locator('.cf-datetime__button')).toHaveText('Datum wählen'); + + // Erst nach den positiven Zusicherungen: Vorher wäre ein noch nicht + // gerendertes Formular ebenfalls „frei von englischen Texten". + const formular = page.locator('#poststuff'); + for (const englisch of ['There are no entries yet', 'Add Distribution', 'Select Date', 'Select...', 'No options']) { + await expect(formular).not.toContainText(englisch); + } + }); + test('blockt die Veröffentlichung ohne Pflichtangaben und benennt die Felder', async ({ page }) => { await page.goto('/wp-admin/post-new.php?post_type=odw_dataset'); diff --git a/tests/test-carbon-fields-l10n.php b/tests/test-carbon-fields-l10n.php new file mode 100644 index 00000000..0e88e4fe --- /dev/null +++ b/tests/test-carbon-fields-l10n.php @@ -0,0 +1,378 @@ + + */ + private array $list; + + /** + * Kopfzeilen. + * + * @var array + */ + private array $head; + + /** + * Legt Einträge und Kopfzeilen fest. + * + * @param array $items Einträge. + * @param array $head Kopfzeilen. + */ + public function __construct( array $items, array $head ) { + $this->list = $items; + $this->head = $head; + } + + /** + * Liefert Einträge und Kopfzeilen nur über den magischen Getter. + * + * @param string $name Eigenschaft. + * @return mixed + */ + public function __get( string $name ) { + if ( 'entries' === $name ) { + return $this->list; + } + if ( 'headers' === $name ) { + return $this->head; + } + return null; + } +} + +/** + * Unit tests for ODW_Carbon_Fields_L10n. + */ +class Test_ODW_Carbon_Fields_L10n extends TestCase { + + /** + * Set up WP_Mock and load the class. + */ + protected function setUp(): void { + \WP_Mock::setUp(); + require_once dirname( __DIR__ ) . '/includes/class-carbon-fields-l10n.php'; + $this->reset_loaded(); + } + + /** + * Tear down WP_Mock. + */ + protected function tearDown(): void { + $this->reset_loaded(); + \WP_Mock::tearDown(); + } + + /** + * Setzt den Merker „deutsche Datei geladen" zurück (statisch, pro Request). + */ + private function reset_loaded(): void { + $prop = new \ReflectionProperty( ODW_Carbon_Fields_L10n::class, 'german_loaded' ); + $prop->setAccessible( true ); + $prop->setValue( null, false ); + } + + /** + * Ein Übersetzungseintrag wie Translation_Entry. + * + * @param string $singular msgid. + * @param string[] $translations msgstr(s). + * @param string|null $context msgctxt. + */ + private function entry( string $singular, array $translations, ?string $context = null ): object { + $entry = new \stdClass(); + $entry->singular = $singular; + $entry->translations = $translations; + $entry->context = $context; + return $entry; + } + + /** + * Liest eine .mo-Datei in ein msgid => msgstr-Array (kleiner Reader für den Test). + * + * @param string $path Pfad. + * @return array + */ + private function read_mo( string $path ): array { + $bin = (string) file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- lokale Datei. + $this->assertSame( 0x950412de, unpack( 'V', substr( $bin, 0, 4 ) )[1], 'Keine gültige .mo (Magic Number).' ); + + $header = unpack( 'Vmagic/Vrev/Vcount/Vorig/Vtrans', substr( $bin, 0, 20 ) ); + $result = array(); + for ( $i = 0; $i < $header['count']; $i++ ) { + $o = unpack( 'Vlen/Voff', substr( $bin, $header['orig'] + 8 * $i, 8 ) ); + $t = unpack( 'Vlen/Voff', substr( $bin, $header['trans'] + 8 * $i, 8 ) ); + $id = substr( $bin, $o['off'], $o['len'] ); + $result[ $id ] = substr( $bin, $t['off'], $t['len'] ); + } + return $result; + } + + // ----------------------------------------------------------------------- + // Locale + // ----------------------------------------------------------------------- + + /** + * Alle deutschen Varianten gelten als deutsch, andere Sprachen nicht. + */ + public function test_is_german_locale(): void { + foreach ( array( 'de', 'de_DE', 'de_DE_formal', 'de_AT', 'de_CH', 'de_CH_informal' ) as $locale ) { + $this->assertTrue( ODW_Carbon_Fields_L10n::is_german_locale( $locale ), $locale ); + } + foreach ( array( 'en_US', 'fr_FR', 'dsb_DE', 'hsb_DE', '' ) as $locale ) { + $this->assertFalse( ODW_Carbon_Fields_L10n::is_german_locale( $locale ), $locale ); + } + } + + /** + * Ohne Argument entscheidet determine_locale(). + */ + public function test_is_german_locale_defaults_to_request_locale(): void { + \WP_Mock::userFunction( 'determine_locale' )->andReturn( 'de_DE_formal' ); + + $this->assertTrue( ODW_Carbon_Fields_L10n::is_german_locale() ); + } + + // ----------------------------------------------------------------------- + // locale_data() + // ----------------------------------------------------------------------- + + /** + * Der Fall, an dem Carbon Fields scheitert: Einträge als Liste, nur über + * __get() erreichbar. Die Schlüssel müssen die msgids sein, nicht 0, 1, 2. + */ + public function test_locale_data_keys_by_msgid_for_wp_translations(): void { + \WP_Mock::userFunction( 'determine_locale' )->andReturn( 'de_DE' ); + + $translations = new ODW_Test_Magic_Translations( + array( + $this->entry( 'There are no entries yet.', array( 'Noch keine Einträge vorhanden.' ) ), + $this->entry( 'Add %s', array( '%s hinzufügen' ) ), + ), + array( 'Plural-Forms' => 'nplurals=2; plural=(n != 1);' ) + ); + + $data = ODW_Carbon_Fields_L10n::locale_data( + $translations, + array( + 'domain' => 'carbon-fields-ui', + 'lang' => 'de_DE', + ) + ); + + $this->assertArrayNotHasKey( 0, $data ); + $this->assertSame( array( 'Noch keine Einträge vorhanden.' ), $data['There are no entries yet.'] ); + $this->assertSame( array( '%s hinzufügen' ), $data['Add %s'] ); + $this->assertSame( 'carbon-fields-ui', $data['']['domain'] ); + $this->assertSame( 'de_DE', $data['']['lang'] ); + $this->assertSame( 'nplurals=2; plural=(n != 1);', $data['']['plural_forms'] ); + } + + /** + * Ältere WordPress-Versionen (MO/Translations): echte Eigenschaften, + * Einträge nach Schlüssel indiziert — Ergebnis identisch. + */ + public function test_locale_data_with_keyed_entries(): void { + \WP_Mock::userFunction( 'determine_locale' )->andReturn( 'de_DE' ); + + $translations = new \stdClass(); + $translations->headers = array(); + $translations->entries = array( + 'Select Date' => $this->entry( 'Select Date', array( 'Datum wählen' ) ), + ); + + $data = ODW_Carbon_Fields_L10n::locale_data( $translations ); + + $this->assertSame( array( 'Datum wählen' ), $data['Select Date'] ); + $this->assertSame( 'carbon-fields-ui', $data['']['domain'] ); + $this->assertArrayNotHasKey( 'plural_forms', $data[''] ); + } + + /** + * Kontext wird wie in @wordpress/i18n mit \u0004 vorangestellt; leere + * Übersetzungen fallen weg, damit das englische Original sichtbar bleibt + * statt eines leeren Buttons. + */ + public function test_locale_data_context_and_empty_translations(): void { + \WP_Mock::userFunction( 'determine_locale' )->andReturn( 'de_DE' ); + + $translations = new ODW_Test_Magic_Translations( + array( + $this->entry( 'Add', array( 'Hinzufügen' ), 'button' ), + $this->entry( 'Clear', array( '' ) ), + $this->entry( '', array( 'Kopf' ) ), + ), + array() + ); + + $data = ODW_Carbon_Fields_L10n::locale_data( $translations ); + + $this->assertSame( array( 'Hinzufügen' ), $data["button\4Add"] ); + $this->assertArrayNotHasKey( 'Clear', $data ); + $this->assertCount( 2, $data ); + } + + /** + * NOOP_Translations (keine Datei geladen, z. B. en_US): nur der Kopf. + */ + public function test_locale_data_without_translations(): void { + \WP_Mock::userFunction( 'determine_locale' )->andReturn( 'en_US' ); + + $this->assertSame( + array( + '' => array( + 'domain' => 'carbon-fields-ui', + 'lang' => 'en_US', + ), + ), + ODW_Carbon_Fields_L10n::locale_data( null ) + ); + } + + // ----------------------------------------------------------------------- + // filter_config() + // ----------------------------------------------------------------------- + + /** + * Deutsch: lädt die gebündelte Datei in die Domain carbon-fields-ui und + * ersetzt config.locale; übrige Einträge von window.cf bleiben unberührt. + */ + public function test_filter_config_loads_german_file(): void { + \WP_Mock::userFunction( 'determine_locale' )->andReturn( 'de_DE' ); + \WP_Mock::userFunction( 'load_textdomain' ) + ->once() + ->with( 'carbon-fields-ui', ODW_PLUGIN_DIR . 'languages/carbon-fields-ui-de_DE.mo' ) + ->andReturn( true ); + \WP_Mock::userFunction( 'get_translations_for_domain' ) + ->with( 'carbon-fields-ui' ) + ->andReturn( + new ODW_Test_Magic_Translations( + array( $this->entry( 'There are no entries yet.', array( 'Noch keine Einträge vorhanden.' ) ) ), + array() + ) + ); + + $config = ODW_Carbon_Fields_L10n::filter_config( + array( + 'config' => array( + 'locale' => array( + '' => array( + 'domain' => 'carbon-fields-ui', + 'lang' => 'de_DE', + ), + 0 => array( 'kaputt' ), + ), + 'pagenow' => 'post.php', + ), + ) + ); + + $this->assertSame( 'post.php', $config['config']['pagenow'] ); + $this->assertArrayNotHasKey( 0, $config['config']['locale'] ); + $this->assertSame( array( 'Noch keine Einträge vorhanden.' ), $config['config']['locale']['There are no entries yet.'] ); + + // Ein zweiter Aufruf im selben Request lädt nicht erneut (once() oben). + ODW_Carbon_Fields_L10n::filter_config( array( 'config' => array() ) ); + } + + /** + * Nicht deutsch: keine deutsche Datei, aber trotzdem korrekt geschlüsselte + * Daten — davon profitieren auch die Übersetzungen, die Carbon Fields + * selbst mitbringt (z. B. fr_FR). + */ + public function test_filter_config_other_locale_does_not_load_german(): void { + \WP_Mock::userFunction( 'determine_locale' )->andReturn( 'fr_FR' ); + \WP_Mock::userFunction( 'load_textdomain' )->never(); + \WP_Mock::userFunction( 'get_translations_for_domain' )->andReturn( + new ODW_Test_Magic_Translations( + array( $this->entry( 'There are no entries yet.', array( "Il n'y a pas encore d'entrées." ) ) ), + array() + ) + ); + + $config = ODW_Carbon_Fields_L10n::filter_config( array( 'config' => array() ) ); + + $this->assertSame( array( "Il n'y a pas encore d'entrées." ), $config['config']['locale']['There are no entries yet.'] ); + } + + /** + * Unerwartete Struktur wird unverändert durchgereicht. + */ + public function test_filter_config_passes_through_unexpected_input(): void { + $this->assertSame( 'x', ODW_Carbon_Fields_L10n::filter_config( 'x' ) ); + $this->assertSame( array( 'other' => 1 ), ODW_Carbon_Fields_L10n::filter_config( array( 'other' => 1 ) ) ); + } + + // ----------------------------------------------------------------------- + // Gebündelte Dateien + // ----------------------------------------------------------------------- + + /** + * Die .mo enthält die Texte aus dem Formular des Plugins und stimmt mit + * der .po überein — eine vergessene Neukompilierung fällt hier auf. + */ + public function test_bundled_german_mo_matches_po(): void { + $mo = $this->read_mo( ODW_Carbon_Fields_L10n::german_mo_path() ); + + $expected = array( + 'There are no entries yet.' => 'Noch keine Einträge vorhanden.', + 'Add %s' => '%s hinzufügen', + 'Select Date' => 'Datum wählen', + 'No options.' => 'Keine Optionen vorhanden.', + 'Collapse All' => 'Alle zuklappen', + 'Expand All' => 'Alle aufklappen', + 'This field is required.' => 'Dieses Feld ist ein Pflichtfeld.', + ); + foreach ( $expected as $msgid => $msgstr ) { + $this->assertSame( $msgstr, $mo[ $msgid ] ?? null, $msgid ); + } + + // Jede msgid der .po steht in der .mo. + $po = (string) file_get_contents( ODW_PLUGIN_DIR . 'languages/carbon-fields-ui-de_DE.po' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- lokale Datei. + preg_match_all( '/^msgid "(.+)"$/m', $po, $matches ); + $this->assertNotEmpty( $matches[1] ); + foreach ( $matches[1] as $msgid ) { + $msgid = stripcslashes( $msgid ); + $this->assertArrayHasKey( $msgid, $mo, 'Nicht kompiliert: ' . $msgid ); + } + + $this->assertStringContainsString( 'Plural-Forms: nplurals=2; plural=(n != 1);', $mo[''] ); + } + + /** + * Die Schlüssel sind die festen Vorgaben von react-select — nur dann + * findet das Skript den Text im DOM. + */ + public function test_react_select_strings(): void { + \WP_Mock::userFunction( '__' )->andReturnArg( 0 ); + + $strings = ODW_Carbon_Fields_L10n::react_select_strings(); + + $this->assertSame( array( 'Select...', 'No options' ), array_keys( $strings ) ); + $this->assertSame( 'Auswählen…', $strings['Select...'] ); + $this->assertSame( 'Keine passenden Optionen', $strings['No options'] ); + } +} From bd00edf44c8402cbeb08a56480b7049c42d6e2b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 12:19:08 +0000 Subject: [PATCH 2/2] fix: Kalendersprache folgt der Locale (en_US englisch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Datumsfelder stellten flatpickr fest auf locale 'de', und odw-datepicker-de.js lud die deutsche Sprachdatei für jede Locale. Auf en_US zeigte der Kalender deshalb deutsche Monats- und Wochentagsnamen. - ODW_Fields::date_picker_options(): 'de' bei deutscher Locale, sonst flatpickrs englisches 'default' (vermeidet zugleich „invalid locale"). - odw-datepicker-de.js nur für deutsche Locales einbinden. - Unit-Test je Locale, E2E: deutsche Wochentage im deutschen Formular. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GcBuax4aJvDgWhT9oi6vNM --- CHANGELOG.md | 10 ++++++-- assets/js/odw-datepicker-de.js | 4 +++- includes/class-admin.php | 19 +++++++++------ includes/class-fields.php | 39 +++++++++++++++++-------------- tests/e2e/admin-workflow.spec.js | 5 ++++ tests/test-carbon-fields-l10n.php | 25 ++++++++++++++++++++ 6 files changed, 74 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b14018e7..78ebf931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,13 +35,19 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.ht Übersetzung. Der bisherige Tausch in `odw-datepicker-de.js` suchte „Select Date“ als Platzhalter des Eingabefelds — es ist aber die Beschriftung eines Buttons, der Tausch blieb deshalb wirkungslos und ist entfallen. +- **Kalender auf englischen Installationen englisch.** Die Datumsfelder stellten flatpickr fest + auf `locale: 'de'`, und `odw-datepicker-de.js` lud die deutsche Sprachdatei für jede Locale — + auf en_US standen deutsche Monats- und Wochentagsnamen neben englischen Beschriftungen. Die + Kalendersprache folgt jetzt der Oberfläche (`ODW_Fields::date_picker_options()`: `de` bei + deutscher Locale, sonst `default`), und die Sprachdatei wird nur für deutsche Locales geladen. ### 🧪 Tests - `tests/test-carbon-fields-l10n.php`: Schlüsselbildung mit WP_Translations (nur `__get()`, Liste) und älteren Übersetzungsobjekten, Laden nur bei deutscher Locale, gebündelte `.mo` - passend zur `.po`. + passend zur `.po`, Kalendersprache je Locale. - E2E (`admin-workflow.spec.js`): Das deutsche Formular zeigt „Noch keine Einträge vorhanden.“, - „Distribution hinzufügen“ und „Datum wählen“ und keinen der englischen Texte. + „Distribution hinzufügen“, „Datum wählen“ und deutsche Wochentage im Kalender, aber keinen der + englischen Texte. --- diff --git a/assets/js/odw-datepicker-de.js b/assets/js/odw-datepicker-de.js index 5651a7ee..e83d137c 100644 --- a/assets/js/odw-datepicker-de.js +++ b/assets/js/odw-datepicker-de.js @@ -3,7 +3,9 @@ * * Registers the German locale on the global flatpickr instance and re-localizes * any pickers that were already initialised, so month and weekday names appear - * in German. + * in German. Enqueued for German locales only (ODW_Admin::enqueue_assets()); + * the date fields pick their calendar locale to match + * (ODW_Fields::date_picker_options()). * * The "Select Date" button is not handled here: it is a regular Carbon Fields * UI string and comes from languages/carbon-fields-ui-de_DE.mo via diff --git a/includes/class-admin.php b/includes/class-admin.php index c5fd9aa0..8dfef8cd 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -453,13 +453,18 @@ public static function enqueue_assets( string $hook ): void { ) ); - wp_enqueue_script( - 'odw-datepicker-de', - ODW_PLUGIN_URL . 'assets/js/odw-datepicker-de.js', - array(), - ODW_VERSION, - true - ); + // Deutsche Monats- und Wochentagsnamen für den Kalender nur bei + // deutscher Oberfläche; die Datumsfelder wählen ihre Kalendersprache + // passend dazu (ODW_Fields::date_picker_options()). + if ( ODW_Carbon_Fields_L10n::is_german_locale() ) { + wp_enqueue_script( + 'odw-datepicker-de', + ODW_PLUGIN_URL . 'assets/js/odw-datepicker-de.js', + array(), + ODW_VERSION, + true + ); + } // Platzhalter und Leermeldung der Mehrfachauswahl (react-select). // Die übrigen Carbon-Fields-Texte kommen über ODW_Carbon_Fields_L10n. diff --git a/includes/class-fields.php b/includes/class-fields.php index a0fc0c88..406af0fb 100644 --- a/includes/class-fields.php +++ b/includes/class-fields.php @@ -390,12 +390,7 @@ static function (): string { // beisammen — unter „Inhaltliche Angaben" war das Datum fehl am Platz. Field::make( 'date', 'odw_issued', __( 'Wann wurden diese Daten zum ersten Mal veröffentlicht?', 'open-data-wizard' ) ) ->set_storage_format( 'Y-m-d' ) - ->set_picker_options( - array( - 'dateFormat' => 'Y-m-d', - 'locale' => 'de', - ) - ) + ->set_picker_options( self::date_picker_options() ) ->set_help_text( __( 'VERÖFFENTLICHUNGSDATUM (dct:issued)', 'open-data-wizard' ) ), // Das Änderungsdatum (dct:modified) hat kein Eingabefeld mehr: @@ -439,22 +434,12 @@ static function (): string { Field::make( 'date', 'odw_temporal_start', __( 'Ab wann reicht der Zeitraum, den die Daten abdecken?', 'open-data-wizard' ) ) ->set_storage_format( 'Y-m-d' ) - ->set_picker_options( - array( - 'dateFormat' => 'Y-m-d', - 'locale' => 'de', - ) - ) + ->set_picker_options( self::date_picker_options() ) ->set_help_text( __( 'ZEITLICHER BEZUG — START (dct:temporal)', 'open-data-wizard' ) ), Field::make( 'date', 'odw_temporal_end', __( 'Bis wann reicht der Zeitraum, den die Daten abdecken?', 'open-data-wizard' ) ) ->set_storage_format( 'Y-m-d' ) - ->set_picker_options( - array( - 'dateFormat' => 'Y-m-d', - 'locale' => 'de', - ) - ) + ->set_picker_options( self::date_picker_options() ) ->set_help_text( __( 'ZEITLICHER BEZUG — ENDE (dct:temporal)', 'open-data-wizard' ) ), Field::make( 'html', 'odw_ext_hint_contact' ) @@ -1016,6 +1001,24 @@ public static function get_license_descriptions(): array { return (array) apply_filters( 'odw_license_descriptions', $descriptions ); } + /** + * Optionen für den Kalender (flatpickr) der Datumsfelder. + * + * Die Sprache folgt der Oberfläche: Deutsch nur bei deutscher Locale, sonst + * das englische Original („default"). Fest auf „de" gestellt zeigte der + * Kalender auch auf en_US deutsche Monate; ohne die Sprachdatei aus + * assets/js/odw-datepicker-de.js, die nur für deutsche Locales geladen + * wird, würde flatpickr bei „de" zudem „invalid locale" melden. + * + * @return array{dateFormat: string, locale: string} + */ + public static function date_picker_options(): array { + return array( + 'dateFormat' => 'Y-m-d', + 'locale' => ODW_Carbon_Fields_L10n::is_german_locale() ? 'de' : 'default', + ); + } + /** * Kuratierte Liste geografischer Regionen (Name → GeoNames-URI) für das * dct:spatial-Feld. Deckt Deutschland, die 16 Bundesländer und größere Städte ab. diff --git a/tests/e2e/admin-workflow.spec.js b/tests/e2e/admin-workflow.spec.js index 41ad2bf1..61790864 100644 --- a/tests/e2e/admin-workflow.spec.js +++ b/tests/e2e/admin-workflow.spec.js @@ -295,6 +295,11 @@ test.describe('Formular', () => { const zeitraum = page.locator('.cf-field', { has: page.locator(cfField('odw_temporal_start')) }); await expect(zeitraum.locator('.cf-datetime__button')).toHaveText('Datum wählen'); + // Kalender (flatpickr, `static: true` — liegt im Feld): deutsche Wochentage, + // Woche beginnt am Montag. Die Kalendersprache folgt seit v2.47.5 der + // Locale (ODW_Fields::date_picker_options()); auf en_US bleibt sie englisch. + await expect(zeitraum.locator('.flatpickr-weekday').first()).toHaveText(/^\s*Mo\s*$/); + // Erst nach den positiven Zusicherungen: Vorher wäre ein noch nicht // gerendertes Formular ebenfalls „frei von englischen Texten". const formular = page.locator('#poststuff'); diff --git a/tests/test-carbon-fields-l10n.php b/tests/test-carbon-fields-l10n.php index 0e88e4fe..808b8c14 100644 --- a/tests/test-carbon-fields-l10n.php +++ b/tests/test-carbon-fields-l10n.php @@ -156,6 +156,31 @@ public function test_is_german_locale_defaults_to_request_locale(): void { $this->assertTrue( ODW_Carbon_Fields_L10n::is_german_locale() ); } + /** + * Kalendersprache der Datumsfelder folgt der Oberfläche: „de" nur bei + * deutscher Locale, sonst flatpickrs englisches Original. + */ + public function test_date_picker_locale_follows_interface(): void { + require_once ODW_PLUGIN_DIR . 'includes/class-fields.php'; + + \WP_Mock::userFunction( 'determine_locale' )->andReturn( 'de_AT', 'en_US' ); + + $this->assertSame( + array( + 'dateFormat' => 'Y-m-d', + 'locale' => 'de', + ), + ODW_Fields::date_picker_options() + ); + $this->assertSame( + array( + 'dateFormat' => 'Y-m-d', + 'locale' => 'default', + ), + ODW_Fields::date_picker_options() + ); + } + // ----------------------------------------------------------------------- // locale_data() // -----------------------------------------------------------------------