From 2a6ad41b56b17d98ff82b1a51062be3d6bc72e02 Mon Sep 17 00:00:00 2001 From: Dennis Ploetner Date: Thu, 6 Aug 2026 10:40:59 +0200 Subject: [PATCH 1/2] Documentation review --- .gitignore | 1 - AGENTS.md | 25 +---- CLAUDE.md | 91 +++++++++++++++++++ Changelog.md | 17 ++++ docs/acknowledgements.md | 5 +- docs/api.md | 2 +- docs/hooks.md | 45 ++++++--- includes/ContentImport/ContentImporter.php | 4 +- .../Importers/ImportersBaseFactory.php | 2 +- includes/RestApi/RestApi.php | 12 +-- 10 files changed, 155 insertions(+), 49 deletions(-) create mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index 6d949297a..dfd75ea48 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ .phpunit.cache .phpunit.result.cache .vscode/ -CLAUDE.md assets/js/msls-widget-block/ assets/js/msls.js composer.lock diff --git a/AGENTS.md b/AGENTS.md index 4b7a67894..a89160906 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,24 +1,5 @@ # AGENTS.md -WordPress plugin providing multilingual support via Multisite - connects content across sites for language switching. - -## Structure -- `MultisiteLanguageSwitcher.php` - Plugin bootstrap (do not modify header) -- `includes/` - Core PHP classes (`lloc\Msls\` namespace) -- `src/` - JavaScript source components -- `assets/` - CSS, JS, flags, images -- `tests/` - PHPUnit + Playwright tests -- `vendor/` - Composer dependencies (do not edit) - -## Conventions -- WordPress Coding Standards (PHPCS) -- Strict typing (`declare(strict_types=1)`) -- Text domain: `msls` - -## CI -PHPStan (strict) | PHPCS | PHPUnit | Playwright | GitHub Actions on PRs - -## Do Not -- Edit `vendor/` or `build/` directories -- Modify language files outside localization workflow -- Change plugin header in `MultisiteLanguageSwitcher.php` \ No newline at end of file +See [CLAUDE.md](CLAUDE.md) — it is the single source of truth for agent +instructions in this repository (project overview, commands, architecture, +and conventions). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..921b43e56 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,91 @@ +# CLAUDE.md + +This file is the single source of truth for coding agents working in this repository (`AGENTS.md` points here). + +## About + +Multisite Language Switcher (MSLS) is a WordPress plugin that adds multilingual support to WordPress multisite installations. It connects content (posts, pages, custom post types, taxonomies) across sites in a multisite network for language switching. + +## Commands + +### Testing +```bash +composer phpunit # Run PHPUnit test suite +composer phpunit -- --filter=TestClassName # Run a single test class +composer phpunit -- --filter=testMethodName # Run a single test method +composer phpunit:clover # Run tests with code coverage (XML) +composer phpunit:html # Run tests with code coverage (HTML) +``` + +### Static Analysis & Linting +```bash +composer phpstan # PHPStan at level 8 +composer phpcs # PHP compatibility check (7.4+) +vendor/bin/phpcs # WordPress coding standards (uses .phpcs.xml.dist) +``` + +### Building +```bash +npm run build # Build JS (uglify + less + Gutenberg block) +npm run build-msls-block # Build only the Gutenberg block +``` + +### E2E Tests +```bash +npx playwright test # Run Playwright tests (against msls.co) +npx playwright test --ui # Run with UI +``` + +### Local Development Environment +```bash +npx wp-env start # Start WordPress multisite via wp-env (PHP 8.3) +npx wp-env stop +``` + +## Architecture + +### Repository Layout +- `MultisiteLanguageSwitcher.php` — plugin bootstrap +- `includes/` — core PHP classes +- `src/` — JavaScript source components +- `assets/` — CSS, JS, flags, images +- `docs/` — developer reference (API, hooks, snippets) +- `tests/` — PHPUnit and Playwright tests + +### Namespace & Autoloading +- PSR-4: `lloc\Msls\` maps to `includes/`, split into per-concern sub-namespaces: `Admin\`, `Blog\`, `Cli\`, `Component\`, `ContentImport\`, `ContentTypes\`, `Data\`, `Db\`, `Frontend\`, `Link\`, `Options\`, `Registry\`, `Request\`, `RestApi\` +- PSR-4 (dev): `lloc\MslsTests\` maps to `tests/phpunit/` +- Plugin bootstrap: `MultisiteLanguageSwitcher.php` — defines constants, then on `plugins_loaded` requires `includes/aliases.php`, `includes/deprecated.php`, and `includes/api.php`, builds the PHP-DI container from `config.php`, and calls `lloc\Msls\Plugin::init()` plus `lloc\Msls\Cli\Cli::init()` +- **Backwards-compatibility aliases**: `includes/aliases.php` registers the ~60 pre-3.0 flat class names (`MslsOptions`, `MslsLink`, `MslsPlugin`, …) as `class_alias()` entries for their namespaced replacements. Write new code against the namespaced names; the aliases exist only for third-party consumers + +### Key Patterns +- **Registry/Singleton**: `Registry\Instance` is the base class providing the `::instance()` static accessor (backed by `Registry\Registry`); `Registry\GetSet` extends it to add overloaded property access +- **Factory methods**: `Options\Options::create()`, `Options\Tax\Tax::create()`, `Options\Query\Query::create()`, `ContentTypes\ContentTypes::create()` return context-aware instances based on WordPress conditional tags (is_category, is_tag, is_day, etc.) +- **Options hierarchy**: `Options\Options` (base, extends `GetSet`) → `Options\Post\Post` (post translations) / `Options\Tax\Tax` → `Options\Tax\Term` → `Options\Tax\Category` (taxonomy translations) / `Options\Query\Query` → `Author`, `Day`, `Month`, `Year`, `PostType` (archive pages) +- **Link rendering**: `Link\Link` base class with variants (`Link\TextOnly`, `Link\ImageOnly`, `Link\TextImage`) — selected by the display index 0–3 from `Link\Link::get_types()`, controlled by admin settings +- **Content Import**: `ContentImport/` subsystem handles duplicating content across sites with importers for post fields, meta, terms, attachments, and thumbnails +- **REST API / Quick Create**: `RestApi/` exposes the endpoints behind the editor metabox button and the "Add from Translation" submenu (`Admin\TranslationPicker\`) + +### Global API Functions +`includes/api.php` exposes the template functions: `msls_the_switcher()`, `msls_get_switcher()`, `msls_get_permalink()`, `msls_get_flag_url()`, `msls_blog_collection()`, etc. Legacy names (`the_msls()`, `get_the_msls()`, …) live in `includes/deprecated.php` and forward to them with a `_deprecated_function()` notice. + +### Developer Documentation +`docs/` holds the reference material: `api.md` (public API functions), `hooks.md` (every action and filter), `snippets.md` (integration recipes), `acknowledgements.md` (credits and translators). Keep these in sync when adding or renaming a hook or an API function. + +### Test Framework +- PHPUnit 10 with Brain\Monkey for WordPress function mocking +- Patchwork for redefining PHP internals (`filter_input`, `filter_input_array`, `filter_has_var`) +- Base test class: `MslsUnitTestCase` — sets up Monkey, stubs common WP escaping/i18n functions +- Tests mirror the source structure with a `Test` prefix: `includes/Options/Tax/Term.php` → `tests/phpunit/Options/Tax/TestTerm.php` + +## CI + +GitHub Actions runs PHPStan, PHPCS, PHPUnit, and Playwright on every pull request. + +## Conventions + +- WordPress Coding Standards enforced via PHPCS (tabs, Yoda conditions, WordPress function spacing) +- All classes use `declare(strict_types=1)` +- Text domain: `multisite-language-switcher` everywhere — in the plugin header and in every `__()` / `esc_html__()` call. Do not use `msls` as a text domain; it is the name of the plugin's option row (`get_option( 'msls' )`) +- Do not modify the plugin header in `MultisiteLanguageSwitcher.php` +- Do not edit `vendor/`, `build/`, `node_modules/`, or language files directly diff --git a/Changelog.md b/Changelog.md index 0a7b5bcbb..5b38852a0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,20 @@ +## 3.0.0 + +* Add Quick Create for translations: create the translated post straight from the editor metabox, or pick a source post on the new "Add from Translation" submenu (single and bulk), backed by a REST endpoint and switchable in the settings. +* Add `msls_quick_create_capability` so integrations can override the Quick Create permission checks, plus filters for the post data, the inserted post, the response, the untranslated-posts list, and the mapped taxonomy terms. +* Add filter hooks for the AJAX suggest results of the post and term metaboxes. +* Restructure `lloc\Msls\` into per-concern sub-namespaces (`Admin\`, `Blog\`, `ContentImport\`, `ContentTypes\`, `Frontend\`, `Link\`, `Options\`, `Registry\`, `RestApi\`). Every former flat `Msls*` class name keeps working through the aliases in `includes/aliases.php`. +* Move the public helper functions into `includes/api.php` and make the `$attr` argument of `msls_get_switcher()` optional. +* Add a PHP-DI container for service construction. +* Documentation: add a developer reference under `docs/` (public API, hooks, snippets, acknowledgements) and refresh the class and package diagrams. +* Fix: check authorization on the destination post during content import, and correct the ContentImporter permission and post type checks. +* Fix: do not fall back to `home_url()` for taxonomy and query archives. +* Fix: broken links on the page for the latest posts. +* Fix: several issues in the blog collection. +* i18n: close gaps in the WP-CLI messages and the Quick Create button title. +* Internal: strict typing throughout, PHPStan level 8 clean, `ABSPATH` guards, Plugin Check and PHPCS findings addressed, wp-env setup for local multisite development. +* Maintenance: numerous dependency updates. + ## 2.10.1 * Fix: Deprecated function warning pointed to non-existent function. * Documentation: update README.md code snippets to reflect new function names. diff --git a/docs/acknowledgements.md b/docs/acknowledgements.md index d2fbf9987..2b2856715 100644 --- a/docs/acknowledgements.md +++ b/docs/acknowledgements.md @@ -10,9 +10,9 @@ banner. Thanks to all translators for their great work. * German (de_DE) - [Dennis Ploetner](http://lloc.de/) -* Italian (it_IT) - [Antonella Cucinelli](http://www.freely.de/) +* Italian (it_IT) - [Antonella Cucinelli](http://www.freely.de/it/) * Dutch (nl_NL) - [Alexandra Kleijn](http://www.buurtaal.de/) -* Brazillian Portuguese (pt_BR) - [Victor](http://www.coolweb.com.br/) +* Brazillian Portuguese (pt_BR) - [Coolweb](http://www.coolweb.com.br/) * Spanish (es_ES) - [Andreas Breitschopp](http://www.ab-weblog.com/en/) * French (fr_FR) - [Andreas Breitschopp](http://www.ab-tools.com/en/) * Russian (ru_RU) - [Andrey Vystavkin](http://j4vk.com/) @@ -36,6 +36,7 @@ Thanks to all translators for their great work. * Arabic (ar) - Mohamed Elwan * Norwegian (nb_NO) - Ibrahim Qraiqe * Bulgarian (bg_BG) - [Vencislav Raev](http://www.catblue.net/) +* Mexican Spanish (es_MX) - [Fernando Mata](https://fernandomata.mx/) You can translate this plugin on [translate.wordpress.org](https://translate.wordpress.org/projects/wp-plugins/multisite-language-switcher/), or if you prefer and have created your own language pack, or have an update of an existing one, you can [send me](mailto:re@lloc.de) your gettext PO diff --git a/docs/api.md b/docs/api.md index 113380a21..c5fcfc769 100644 --- a/docs/api.md +++ b/docs/api.md @@ -143,7 +143,7 @@ mentioned only for completeness. ## Deprecated functions -The following pre-2.10.1 names live in `includes/deprectated.php`. Each one +The following pre-2.10.1 names live in `includes/deprecated.php`. Each one still works but emits a `_deprecated_function()` notice and simply forwards to its modern `msls_*` replacement. Update calls in your code at your earliest convenience. diff --git a/docs/hooks.md b/docs/hooks.md index ac809e7d9..2239a6ba9 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -23,10 +23,14 @@ switcher renderer for the entire site or per template. ### msls_output_get -Filter applied to the HTML link for every individual language item before it -joins the output array. Use it to wrap, decorate, or replace the per-language -anchor — for example to add a CSS class, swap in a button element, or -append a flag image only on the current language. +Filter that builds the markup for every individual language item before it +joins the output array. It receives the target URL (not the finished anchor), +the `LinkInterface` object, and whether the item points at the current blog — +so the return value has to be the complete HTML for that item. When no +callback is attached, MSLS renders its own default anchor instead. Use it to +wrap, decorate, or replace the per-language link — for example to add a CSS +class, swap in a button element, or append a flag image only on the current +language. ### msls_output_get_tags @@ -356,6 +360,15 @@ that expects another name shape. ## REST API and Quick Create +### msls_quick_create_capability + +Filter on the result of the Quick Create capability check. Alongside the +default decision you get the source post ID (`0` for list-style checks), the +source and target blog IDs, and a `$context` of either `read` (checking access +to the source post) or `create` (checking the right to insert on the target +blog). Use it to let a translator without an account on the source blog mirror +a post into the target blog, or to tighten the default checks. + ### msls_quick_create_post_data Filter on the post data array (title, content, status, post type, …) that @@ -444,22 +457,26 @@ applies across every import, not just the current one. ### msls_content_import_{type}_importer -Dynamic filter, with `{type}` being one of `post-fields`, `post-meta`, -`terms`, `post-thumbnail`, `attachments`, etc. Returning an `Importer` -instance here forces the factory to use that importer for the corresponding -content type, bypassing the slug-based selection. +Dynamic filter, with `{type}` being the type of one of the five importer +factories: `post-fields`, `post-meta`, `terms`, `post-thumbnail`, or +`attachments`. Returning an `Importer` instance here forces the factory to use +that importer for the corresponding content type, bypassing the slug-based +selection. ### msls_content_import_{type}_importers_map -Dynamic filter, with `{type}` matching one of the factories. It filters the -map of available importer implementations (slug ⇒ class) for that content -type. Use it to register a new importer flavor alongside the built-ins. +Dynamic filter, with `{type}` being one of the five factory types listed +above. It filters the map of available importer implementations +(slug ⇒ class) for that content type. Use it to register a new importer +flavor alongside the built-ins. -### msls_content_import_{slug}_selected +### msls_content_import_{type}_selected Dynamic filter that picks which importer slug is "selected" for a given -factory. Use it to programmatically switch between competing importer -implementations — for example based on the post type or destination blog. +factory — `{type}` is again one of the five factory types. The default is the +first entry of the factory's importers map. Use it to programmatically switch +between competing importer implementations, for example based on the post type +or destination blog. ### msls_content_import_log_writer diff --git a/includes/ContentImport/ContentImporter.php b/includes/ContentImport/ContentImporter.php index 44198c4c5..2174890dd 100644 --- a/includes/ContentImport/ContentImporter.php +++ b/includes/ContentImport/ContentImporter.php @@ -308,7 +308,7 @@ public function import_content( ImportCoordinates $import_coordinates, array $po * @param array $post_fields * @param ImportCoordinates $import_coordinates * - * @since TBD + * @since 2.4.0 */ $post_fields = apply_filters( 'msls_content_import_data_before_import', $post_fields, $import_coordinates ); @@ -359,7 +359,7 @@ public function import_content( ImportCoordinates $import_coordinates, array $po * @param ImportLogger $logger * @param Relations $relations * - * @since TBD + * @since 2.4.0 */ do_action( self::MSLS_AFTER_IMPORT_ACTION, $import_coordinates, $logger, $relations ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- constant value is already prefixed with "msls_". diff --git a/includes/ContentImport/Importers/ImportersBaseFactory.php b/includes/ContentImport/Importers/ImportersBaseFactory.php index c26fec4ea..453578ba5 100644 --- a/includes/ContentImport/Importers/ImportersBaseFactory.php +++ b/includes/ContentImport/Importers/ImportersBaseFactory.php @@ -54,7 +54,7 @@ public function make( ImportCoordinates $import_coordinates ) { * @param array $map A map of importers in the shape [ => ] * @param ImportCoordinates $import_coordinates * - * @since TBD + * @since 2.4.0 */ $map = apply_filters( "msls_content_import_{$type}_importers_map", $this->importers_map, $import_coordinates ); diff --git a/includes/RestApi/RestApi.php b/includes/RestApi/RestApi.php index 7e6138c7c..dc17e9236 100644 --- a/includes/RestApi/RestApi.php +++ b/includes/RestApi/RestApi.php @@ -252,7 +252,7 @@ private static function apply_capability_filter( bool $default_cap, int $source_ * @param int $target_blog_id Target blog ID. * @param string $context 'read' when checking the source, 'create' when checking the target. * - * @since TBD + * @since 3.0.0 */ return (bool) apply_filters( 'msls_quick_create_capability', @@ -299,7 +299,7 @@ public function create_translation( \WP_REST_Request $request ) { * @param int $source_blog_id The source blog ID. * @param int $target_blog_id The target blog ID. * - * @since TBD + * @since 3.0.0 */ $post_data = apply_filters( 'msls_quick_create_post_data', $post_data, $source_post, $source_blog_id, $target_blog_id ); @@ -333,7 +333,7 @@ public function create_translation( \WP_REST_Request $request ) { * @param int $source_blog_id The source blog ID. * @param int $target_blog_id The target blog ID. * - * @since TBD + * @since 3.0.0 */ do_action( 'msls_quick_create_after_insert', $new_post_id, $source_post, $source_blog_id, $target_blog_id ); @@ -358,7 +358,7 @@ public function create_translation( \WP_REST_Request $request ) { * @param int $source_blog_id The source blog ID. * @param int $target_blog_id The target blog ID. * - * @since TBD + * @since 3.0.0 */ $response_data = apply_filters( 'msls_quick_create_response', @@ -440,7 +440,7 @@ public function list_untranslated_posts( \WP_REST_Request $request ) { * @param int $target_blog_id Target blog ID. * @param string $post_type Post type queried. * - * @since TBD + * @since 3.0.0 */ $items = apply_filters( 'msls_untranslated_posts', $items, $source_blog_id, $target_blog_id, $post_type ); @@ -549,7 +549,7 @@ protected function prepare_taxonomies( * @param int $source_blog_id The source blog ID. * @param int $target_blog_id The target blog ID. * - * @since TBD + * @since 3.0.0 */ $post_data['_msls_tax_input'] = apply_filters( 'msls_quick_create_tax_input', From bfd8980c9bc05925d78783ea71e5841a30133ba3 Mon Sep 17 00:00:00 2001 From: Dennis Ploetner Date: Thu, 6 Aug 2026 10:48:51 +0200 Subject: [PATCH 2/2] Ignore CLAUDE.md in dist --- .distignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.distignore b/.distignore index 50a350c31..04d2425f6 100644 --- a/.distignore +++ b/.distignore @@ -21,6 +21,7 @@ /src /tests AGENTS.md +CLAUDE.md Changelog.md Diagrams.md README.md