diff --git a/.gitignore b/.gitignore index 8285c4f..32d1889 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,10 @@ dist/ builds/ *.zip +# Development Documentation +# Internal notes and summaries, not for distribution +dev-docs/ + # Environment Files .env .env.local diff --git a/README-DEV.md b/README-DEV.md index 5769e40..d1b7beb 100644 --- a/README-DEV.md +++ b/README-DEV.md @@ -51,7 +51,6 @@ npm run build This command: - - Runs `wp-scripts build --output-path=assets/build` - Uses the custom `webpack.config.js` to ensure output is named `editor-blocks.js` - Generates additional asset files in `assets/build/` @@ -87,7 +86,7 @@ Press `Ctrl+C` to stop the server. ### Project Structure -``` +```tree swiss-football-matchdata/ ├── src/ │ ├── editor-blocks.js # Main editor block components @@ -97,10 +96,11 @@ swiss-football-matchdata/ ├── blocks/ # Block definitions (metadata) │ ├── context/ │ ├── match-events/ +│ ├── match-bench/ +│ ├── match-referees/ │ ├── match-roster/ │ ├── schedule/ │ ├── standings/ -│ ├── team-data/ │ └── shortcode-inserter/ ├── assets/ │ ├── build/ @@ -130,6 +130,7 @@ swiss-football-matchdata/ ### Block Files Each block is defined in its own directory under `blocks/`: + - `block.json` — Block metadata (name, icon, attributes, supports) - The React component code lives in `src/editor-blocks.js` @@ -141,12 +142,14 @@ Each block is defined in its own directory under `blocks/`: - Update attribute definitions 2. **Test with hot reload**: + ```bash npm run start ``` - - Hard-refresh your WordPress editor page - - Make changes in `src/editor-blocks.js` - - The browser auto-updates (HMR) + +- Hard-refresh your WordPress editor page +- Make changes in `src/editor-blocks.js` +- The browser auto-updates (HMR) 3. **Verify output**: - Insert/edit the block in WordPress @@ -154,6 +157,7 @@ Each block is defined in its own directory under `blocks/`: - Look at page source to verify shortcode is generated correctly 4. **Build for production**: + ```bash npm run build ``` @@ -190,36 +194,107 @@ Translation files include: **Workflow when adding new translatable strings:** -1. Add translation strings to PHP or JavaScript with proper functions: +1. Add translation strings to PHP or JavaScript with proper functions and translator comments: ```php - __('Text to translate', 'swi_foot_matchdata') + /* translators: %s is the API error message */ + __('Error loading data: %s', 'swi_foot_matchdata') + _e('Text to display', 'swi_foot_matchdata') ``` -2. Extract strings to the `.pot` template file: - ```bash - wp i18n make-pot . languages/swi_foot_matchdata.pot - ``` - (Requires WP-CLI installed) + **Important**: Always add `/* translators: ... */` comments above i18n functions with placeholders to clarify their meaning for translators. -3. Update existing `.po` translation files from the new template (done by translators in PoEdit or similar) +2. Extract strings and update translation files using the i18n management script: -4. Generate binary `.mo` files: ```bash - wp i18n make-mo languages/ + ./dev-scripts/i18n-manage.sh extract ``` -5. Commit both `.po` and `.mo` files to git: + This extracts all strings to `languages/swi_foot_matchdata.pot`. + +3. Generate `.po` files for translation: + + ```bash + ./dev-scripts/i18n-manage.sh translate + ``` + + Creates/updates `.po` files for: German (de_DE), French (fr_FR), Italian (it_IT), English (en_US) + +4. Provide `.po` files to translators: + + - Translators edit `languages/swi_foot_matchdata-de_DE.po`, `-fr_FR.po`, `-it_IT.po` + - They can use PoEdit, Crowdin, or any PO file editor + - Translator comments help clarify placeholder meanings + +5. After translations are complete, compile `.mo` files: + + ```bash + ./dev-scripts/i18n-manage.sh build + ``` + + Generates binary `.mo` files from `.po` files. + +6. Commit both `.po` and `.mo` files to git: + ```bash git add languages/*.po languages/*.mo git commit -m "Update translations for [feature/language]" ``` +7. (Optional) Clean up regional variants: + + ```bash + ./dev-scripts/i18n-manage.sh clean + ``` + + Removes regional variants (de_AT, de_CH) to keep only core languages. + **Note**: Editor backup files (`.po~`) are **not** committed (ignored by `.gitignore`). The `.mo` files are essential for distribution — they're included in both git commits and distribution packages so installations work immediately without requiring a build step. +### Complete i18n Workflow with Script + +The `i18n-manage.sh` script automates the entire translation pipeline: + +```bash +# Run complete workflow (extract → translate → build → clean) +./dev-scripts/i18n-manage.sh all +``` + +This is equivalent to running: +```bash +./dev-scripts/i18n-manage.sh extract # Extract strings to .pot +./dev-scripts/i18n-manage.sh translate # Generate .po files for each language +./dev-scripts/i18n-manage.sh build # Compile .po to .mo files +./dev-scripts/i18n-manage.sh clean # Remove regional variants +``` + +**Supported Languages (by default)**: +- de_DE — German +- fr_FR — French +- it_IT — Italian +- en_US — English + +**Script Configuration**: + +Edit `dev-scripts/i18n-manage.sh` lines 39-60 to customize: +- `WP_CLI_CMD` — WordPress CLI command (for Docker or local) +- `LANGUAGES` — Supported language codes +- `REGIONAL_VARIANTS` — Variants to remove during cleanup + +**Best Practices**: + +- Always add translator comments for strings with placeholders +- Use ordered placeholder syntax: `%1$d`, `%2$s` (not just `%d`, `%s`) +- Keep strings complete and translatable (don't break into fragments) +- Commit `.po` and `.mo` files to git along with source changes + +For detailed documentation, see: +- `dev-docs/I18N_QUICK_REFERENCE.md` — Quick start guide +- `dev-docs/I18N_OPTIMIZATIONS.md` — Best practices and examples + ## Testing in Development ### Local WordPress Installation @@ -283,10 +358,11 @@ test -f assets/build/editor-blocks.asset.php && echo "✓ Asset manifest present A convenience bash script is included to automate distribution packaging: ```bash -./build-distribution.sh +./dev-scripts/build-distribution.sh ``` This script: + - Creates a clean working directory - Excludes all development files and dependencies - Includes only files needed for distribution @@ -294,11 +370,13 @@ This script: - Displays summary information **Excluded from the distribution**: + - `node_modules/` (development dependencies) - `src/` (source code for building) - `test/` (test files) - `package.json` / `package-lock.json` - `webpack.config.js` (build config) +- `dev-scripts/` (development scripts) - `README-DEV.md` (developer guide) - `.git`, `.gitignore`, `.DS_Store`, and other system files @@ -360,6 +438,7 @@ unzip -l dist/swiss-football-matchdata.zip | head -30 ### 5. Size Check The distribution ZIP should be reasonable in size: + - **With `assets/build/editor-blocks.js`**: ~150–300 KB - **Typical unzipped size**: ~800 KB–2 MB @@ -440,6 +519,7 @@ jobs: ``` This ensures: + - Every push/PR has a valid build - Missing build artifacts fail visibly - PHP code is syntactically correct @@ -501,7 +581,8 @@ npm start | Install deps | `npm install` | | Build for production | `npm run build` | | Development with hot reload | `npm start` | -| Create distribution ZIP | `./build-distribution.sh` | +| Extract and translate strings | `./dev-scripts/i18n-manage.sh all` | +| Create distribution ZIP | `./dev-scripts/build-distribution.sh` | | Check PHP syntax | `php -l includes/class-swi-foot-blocks.php` | | Fix linting issues | `npx eslint src/ --fix` | @@ -512,6 +593,7 @@ This plugin integrates with the Swiss Football Association Club API. The API str **Swagger UI**: https://stg-club-api-services.football.ch/swagger/index.html This documentation provides: + - All available REST endpoints - Request/response schemas - Authentication requirements @@ -519,6 +601,7 @@ This documentation provides: - Example requests and responses Refer to this documentation when: + - Adding new API integrations - Understanding the data structures used in the plugin - Debugging API-related issues diff --git a/README.md b/README.md index d455359..78be8b5 100644 --- a/README.md +++ b/README.md @@ -35,19 +35,7 @@ This section explains how the plugin's Gutenberg editor blocks are intended to b - Works in any paragraph or text-based block. - The shortcode generates dynamically at render time on the frontend. -#### 2. Team Data Block - -- **Purpose**: Create and insert shortcodes using a guided block interface. -- **How to use**: - 1. Insert the `Swiss Football Team Data` block in your post/page. - 2. Open the Inspector Controls (right sidebar). - 3. Select a team from the dropdown. - 4. Choose what to display (Standings or Match). - 5. If you chose Match, select the specific match. - 6. Save the post — the block generates and saves the appropriate shortcode. -- **Best for**: Users who prefer a visual block interface over toolbars. - -#### 3. Context Provider Block (for scoped data) +#### 2. Context Provider Block (for scoped data) - **Purpose**: Provide shared contextual data (season, club/team selection) for child blocks placed inside it. It saves JSON to the frontend as a `data-swi-foot-context` attribute on the provider's wrapper element. - **How to use**: @@ -56,7 +44,7 @@ This section explains how the plugin's Gutenberg editor blocks are intended to b 3. Drop child blocks inside the Context block — these child blocks will inherit the contextual settings. 4. On save, the provider writes a `data-swi-foot-context` attribute containing a compact JSON string with the configured keys and values. This attribute is used by frontend renderers. -#### 4. Specialized Blocks (Standings, Schedule, Roster, Events) +#### 3. Specialized Blocks (Standings, Schedule, Roster, Events) - **Swiss Football Standings** - Purpose: Display a league standings table for a team. @@ -80,15 +68,76 @@ This section explains how the plugin's Gutenberg editor blocks are intended to b ## Shortcodes -Shortcodes provide additional flexibility where blocks are not desirable. Examples: +Shortcodes provide additional flexibility where blocks are not desirable. + +### Context Inheritance + +Most shortcodes listed below support **context inheritance** from a parent `[swi_foot_context]` block. When a `match_id` or `team_id` is not explicitly provided as a parameter, the shortcode will attempt to use values from the current block context. + +**Example with context:** +```html +[swi_foot_context match_id="12345"] + [swi_foot_events] + [swi_foot_roster side="home"] +[/swi_foot_context] +``` + +**Example without context (explicit parameters required):** +```html +[swi_foot_events match_id="12345"] +[swi_foot_roster match_id="12345" side="home"] +``` + +### Match Display & Elements ```html [swi_foot_match match_id="12345"] [swi_foot_match_home_team match_id="12345"] +[swi_foot_match_away_team match_id="12345"] [swi_foot_match_date match_id="12345" format="d.m.Y"] +[swi_foot_match_time match_id="12345"] +[swi_foot_match_venue match_id="12345"] +[swi_foot_match_score match_id="12345"] +[swi_foot_match_status match_id="12345"] +[swi_foot_match_league match_id="12345"] +[swi_foot_match_round match_id="12345"] ``` -Refer to the original `readme.txt` for a complete list of shortcodes and their parameters. +### Team & Standings + +```html +[swi_foot_standings team_id="42"] +``` + +### Match Roster & Bench + +```html +[swi_foot_roster match_id="12345" side="home|away" starting_squad="true|false" bench="true|false"] +[swi_foot_bench match_id="12345"] +``` + +- `[swi_foot_roster]` — Display players for a match. + - `side="home"` or `side="away"` — Show only one team (defaults to "home"). + - `starting_squad="true|false"` — Include starting squad (defaults to true). + - `bench="true|false"` — Include bench/substitutes (defaults to false). + - Players are distinguished by `assignmentRoleId`: 0 = starting squad, non-zero = bench. + - When both are shown, lists are separated by a horizontal rule. + +- `[swi_foot_bench]` — Display team staff and substitutes. + - Automatically shows both home and away team staff. + - `match_id` required; will auto-populate from context if in a context provider. + +### Match Events & Referees + +```html +[swi_foot_events match_id="12345" event_order="dynamic|newest_first|oldest_first"] +[swi_foot_referees match_id="12345"] +``` + +- `[swi_foot_events]` — Show match events (goals, cards, substitutions, etc.) with configurable sort order. +- `[swi_foot_referees]` — Display match referees with their roles formatted as "Role: Firstname Name" (name in bold). + +Refer to the original `readme.txt` for additional shortcode details and parameters. ## Installation @@ -104,28 +153,6 @@ Refer to the original `readme.txt` for a complete list of shortcodes and their p - **Verein ID / Season ID**: used to scope team/season data. - **Cache Duration**: how long API responses are cached (default is 30 seconds; configurable). -## Developer Notes - -- Block registrations and editor scripts live in: - - [includes/class-swi-foot-blocks.php](includes/class-swi-foot-blocks.php) - - [assets/editor-blocks.js](assets/editor-blocks.js) - - [assets/build/index.js](assets/build/index.js) - -- The editor script includes defensive `safeRegisterBlockType` and `safeRegisterFormatType` wrappers to avoid runtime errors when WordPress' editor APIs are not available. -- Blocks are written to use `apiVersion: 3` and expect modern Gutenberg APIs. - -## Headless / Automated Checks - -For quick verification that the built bundle registers the plugin's blocks and formats, a small Node-based test helper exists at `test/register-check.js`. - -Run it locally with: - -```bash -node test/register-check.js -``` - -This script stubs a minimal `window.wp` environment to confirm that `swi-foot/context` and the inline format are registered by the built bundle. - ## Support For support or bug reports, open an issue in the project's tracker or contact the plugin author. diff --git a/assets/admin.css b/assets/admin.css index 159a4d0..2cb3804 100644 --- a/assets/admin.css +++ b/assets/admin.css @@ -12,6 +12,35 @@ color: #23282d; } +/* Form actions with buttons */ +.swi-foot-form-actions { + display: flex; + gap: 10px; + margin-top: 20px; + margin-bottom: 10px; +} + +.swi-foot-form-actions button { + flex-shrink: 0; +} + +/* Connection status messages */ +#connection-status { + display: inline-block; + margin-left: 15px; + font-weight: 500; + line-height: 1.5; + min-height: 22px; +} + +#connection-status.success { + color: #046b3a; +} + +#connection-status.error { + color: #d63638; +} + /* Teams grid layout */ .swi-foot-teams-grid { display: grid; diff --git a/assets/admin.js b/assets/admin.js index 867da5b..77bf72a 100644 --- a/assets/admin.js +++ b/assets/admin.js @@ -2,6 +2,96 @@ jQuery(document).ready(function($) { 'use strict'; + // Settings form with Save and Test functionality + $('#swi-foot-settings-form').on('submit', function(e) { + var $form = $(this); + var $status = $('#connection-status'); + var action = $('[name="swi_foot_action"]:focus').val() || 'save_only'; + + e.preventDefault(); + + // Get form data + var formData = new FormData($form[0]); + + // Submit form via WordPress options.php + fetch($form.attr('action'), { + method: 'POST', + credentials: 'same-origin', + body: formData + }).then(function(resp) { + // After form saves successfully, test if requested + if (action === 'save_and_test') { + return test_api_connection($status); + } else { + $('
Settings saved!
Error saving settings: ' + err.message + '
API credentials not configured. Please configure Username and Password.
API Base URL not configured.
Settings saved and connection test successful!
' + errorMsg + details + '
API URL not reachable: ' + apiUrl + '
Settings saved! The plugin will automatically refresh API tokens as needed.
' . __('How long to cache match data in seconds (10-300)', 'swi_foot_matchdata') . '
'; } + public function language_render() + { + $language = get_option('swi_foot_api_language', '1'); + echo ''; + echo '' . __('Select the language for API responses (German, French, or Italian)', 'swi_foot_matchdata') . '
'; + } + public function admin_scripts($hook) { if ($hook === 'settings_page_swiss-football-matchdata') { @@ -248,23 +261,21 @@ class Swi_Foot_Admin-
' . - sprintf(__('Error loading teams: %s', 'swi_foot_matchdata'), $teams->get_error_message()) . + sprintf( + /* translators: %s is the error message from the API */ + __('Error loading teams: %s', 'swi_foot_matchdata'), + $teams->get_error_message() + ) . '
'; return; } @@ -357,7 +372,12 @@ class Swi_Foot_Admin $cache_count = is_array($keys) ? count($keys) : 0; $cache_duration = get_option('swi_foot_match_cache_duration', 30); - echo '' . sprintf(__('Currently caching %d match records with %d second cache duration.', 'swi_foot_matchdata'), $cache_count, $cache_duration) . '
'; + echo '' . sprintf( + /* translators: %1$d is the number of cached match records, %2$d is the cache duration in seconds */ + __('Currently caching %1$d match records with %2$d second cache duration.', 'swi_foot_matchdata'), + $cache_count, + $cache_duration + ) . '
'; if ($cache_count > 0) { $timestamps = array(); @@ -460,7 +480,5 @@ class Swi_Foot_Admin \ No newline at end of file diff --git a/includes/class-swi-foot-api.php b/includes/class-swi-foot-api.php index 3c93248..5a28aa7 100644 --- a/includes/class-swi-foot-api.php +++ b/includes/class-swi-foot-api.php @@ -9,6 +9,7 @@ class Swi_Foot_API private $verein_id; private $season_id; private $cache_duration; + private $language; public function __construct() { @@ -18,6 +19,7 @@ class Swi_Foot_API $this->verein_id = get_option('swi_foot_verein_id'); $this->season_id = get_option('swi_foot_season_id', date('Y')); $this->cache_duration = get_option('swi_foot_match_cache_duration', 30); + $this->language = get_option('swi_foot_api_language', '1'); // AJAX actions were migrated to REST endpoints (see includes/class-swi-foot-rest.php) @@ -176,6 +178,7 @@ class Swi_Foot_API $response = wp_remote_get($url, array( 'headers' => array( 'X-User-Token' => $token, + 'X-User-Language' => $this->language, 'Content-Type' => 'application/json' ), 'timeout' => 30 @@ -204,7 +207,13 @@ class Swi_Foot_API if ($response_code !== 200) { $body_debug = wp_remote_retrieve_body($response); error_log('Swiss Football API: Request to ' . $url . ' returned ' . $response_code . ' - body: ' . substr($body_debug, 0, 1000)); - return new WP_Error('api_error', 'API request failed with code ' . $response_code); + + // Handle 406 Not Acceptable - data not yet available + if ($response_code === 406) { + return new WP_Error('data_not_available', 'Data not yet available', array('status_code' => 406)); + } + + return new WP_Error('api_error', 'API request failed with code ' . $response_code, array('status_code' => $response_code)); } $body = wp_remote_retrieve_body($response); @@ -348,6 +357,11 @@ class Swi_Foot_API return $this->api_request('/api/match/' . $match_id . '/events'); } + public function get_match_referees($match_id) + { + return $this->api_request('/api/match/' . $match_id . '/referees'); + } + public function get_team_picture($team_id) { // Special handling for team picture endpoint which returns 200 with data or 204 No Content @@ -364,6 +378,7 @@ class Swi_Foot_API $response = wp_remote_get($url, array( 'headers' => array( 'X-User-Token' => $token, + 'X-User-Language' => $this->language, 'Content-Type' => 'application/json' ), 'timeout' => 30 @@ -410,9 +425,9 @@ class Swi_Foot_API return $body; } - public function get_commons_ids() + public function get_common_ids($params = array()) { - return $this->api_request('/api/commons/ids'); + return $this->api_request('/api/common/ids', $params); } /** @@ -441,17 +456,54 @@ class Swi_Foot_API */ public function test_connection() { - // Actually test the connection by making an API request - $result = $this->api_request('/api/teams', array('vereinId' => $this->verein_id)); - - // Check if the request was successful (not an error) - if (is_wp_error($result)) { - error_log('Swiss Football API: Connection test failed - ' . $result->get_error_message()); - return false; + // First, check if credentials are configured + if (empty($this->username) || empty($this->password)) { + return array( + 'success' => false, + 'error' => 'API credentials not configured', + 'details' => 'Please configure API Username and Password in Settings → Swiss Football' + ); + } + + // Check if Verein ID is configured + if (empty($this->verein_id)) { + return array( + 'success' => false, + 'error' => 'Verein ID (Club ID) not configured', + 'details' => 'Please configure Verein ID in Settings → Swiss Football' + ); } - // Verify we got a response (array means success) - return is_array($result); + // Test connection using the /api/common/ids endpoint with required parameters + $result = $this->get_common_ids(array( + 'ClubId' => $this->verein_id, + 'Language' => 1 // 1 = German + )); + + if (is_wp_error($result)) { + $error_msg = $result->get_error_message(); + error_log('Swiss Football API: Connection test failed - ' . $error_msg); + + return array( + 'success' => false, + 'error' => 'API connection failed', + 'details' => $error_msg + ); + } + + if (!is_array($result)) { + return array( + 'success' => false, + 'error' => 'Invalid API response', + 'details' => 'The API returned unexpected data format' + ); + } + + return array( + 'success' => true, + 'error' => null, + 'details' => 'Connection successful!' + ); } public function get_current_match($team_id) diff --git a/includes/class-swi-foot-blocks.php b/includes/class-swi-foot-blocks.php index 6771cc1..8759e85 100644 --- a/includes/class-swi-foot-blocks.php +++ b/includes/class-swi-foot-blocks.php @@ -17,6 +17,23 @@ class Swi_Foot_Blocks public function register_blocks() { + // Register custom block category for Swiss Football blocks + add_filter('block_categories_all', function($categories) { + // Check if category already exists + foreach ($categories as $cat) { + if ($cat['slug'] === 'swi-football') { + return $categories; // Already registered + } + } + // Add custom category with translated label + $categories[] = array( + 'slug' => 'swi-football', + 'title' => __('Swiss Football', 'swi_foot_matchdata'), + 'icon' => 'soccer' + ); + return $categories; + }); + // Register blocks from metadata (block.json) and provide server-side render callbacks $base = dirname(__DIR__) . '/blocks'; @@ -90,14 +107,6 @@ class Swi_Foot_Blocks ) ); } - // Register additional blocks (editor-only save shortcodes) - if ( file_exists( $base . '/team-data/block.json' ) ) { - register_block_type_from_metadata( $base . '/team-data', array( - 'editor_script' => 'swi-foot-editor-blocks', - 'editor_style' => 'swi-foot-editor-styles' - ) ); - } - if ( file_exists( $base . '/match-roster/block.json' ) ) { register_block_type_from_metadata( $base . '/match-roster', array( 'render_callback' => array( $this, 'render_match_roster_block' ), @@ -113,6 +122,22 @@ class Swi_Foot_Blocks 'editor_style' => 'swi-foot-editor-styles' ) ); } + + if ( file_exists( $base . '/match-bench/block.json' ) ) { + register_block_type_from_metadata( $base . '/match-bench', array( + 'render_callback' => array( $this, 'render_match_bench_block' ), + 'editor_script' => 'swi-foot-editor-blocks', + 'editor_style' => 'swi-foot-editor-styles' + ) ); + } + + if ( file_exists( $base . '/match-referees/block.json' ) ) { + register_block_type_from_metadata( $base . '/match-referees', array( + 'render_callback' => array( $this, 'render_match_referees_block' ), + 'editor_script' => 'swi-foot-editor-blocks', + 'editor_style' => 'swi-foot-editor-styles' + ) ); + } } public function enqueue_scripts() @@ -223,7 +248,11 @@ class Swi_Foot_Blocks if (is_wp_error($standings)) { return '{__('Team is inherited from the container context.', 'swi_foot_matchdata')}
-{__('Standings', 'swi_foot_matchdata')}
+{__('League standings inherited from team context. ✓ Data will render on the front-end.', 'swi_foot_matchdata')}
+{__('Team is inherited from the container context.', 'swi_foot_matchdata')}
{__('Schedule', 'swi_foot_matchdata')}
+{__('Team and match schedule inherited from context. ✓ Data will render on the front-end.', 'swi_foot_matchdata')}
+{__('Match Roster', 'swi_foot_matchdata')}
+{__('Team and match are inherited from the container context. ✓ Data will render on the front-end.', 'swi_foot_matchdata')}
+{__('Match Events', 'swi_foot_matchdata')}
+{__('Live match events inherited from context. ✓ Will update during match.', 'swi_foot_matchdata')}
+{__('Match is inherited from the container context.', 'swi_foot_matchdata')}
+{__('Match Bench', 'swi_foot_matchdata')}
+{__('Team bench and substitutes inherited from context. ✓ Data will render on the front-end.', 'swi_foot_matchdata')}
+{__('Match is inherited from the container context.', 'swi_foot_matchdata')}
+{__('Match Referees', 'swi_foot_matchdata')}
+{__('Match officials and referees inherited from context. ✓ Data will render on the front-end.', 'swi_foot_matchdata')}
+