Skip to content

Commit 2a6ad41

Browse files
committed
Documentation review
1 parent 62b8ac9 commit 2a6ad41

10 files changed

Lines changed: 155 additions & 49 deletions

File tree

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
.phpunit.cache
66
.phpunit.result.cache
77
.vscode/
8-
CLAUDE.md
98
assets/js/msls-widget-block/
109
assets/js/msls.js
1110
composer.lock

AGENTS.md

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,5 @@
11
# AGENTS.md
22

3-
WordPress plugin providing multilingual support via Multisite - connects content across sites for language switching.
4-
5-
## Structure
6-
- `MultisiteLanguageSwitcher.php` - Plugin bootstrap (do not modify header)
7-
- `includes/` - Core PHP classes (`lloc\Msls\` namespace)
8-
- `src/` - JavaScript source components
9-
- `assets/` - CSS, JS, flags, images
10-
- `tests/` - PHPUnit + Playwright tests
11-
- `vendor/` - Composer dependencies (do not edit)
12-
13-
## Conventions
14-
- WordPress Coding Standards (PHPCS)
15-
- Strict typing (`declare(strict_types=1)`)
16-
- Text domain: `msls`
17-
18-
## CI
19-
PHPStan (strict) | PHPCS | PHPUnit | Playwright | GitHub Actions on PRs
20-
21-
## Do Not
22-
- Edit `vendor/` or `build/` directories
23-
- Modify language files outside localization workflow
24-
- Change plugin header in `MultisiteLanguageSwitcher.php`
3+
See [CLAUDE.md](CLAUDE.md) — it is the single source of truth for agent
4+
instructions in this repository (project overview, commands, architecture,
5+
and conventions).

CLAUDE.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# CLAUDE.md
2+
3+
This file is the single source of truth for coding agents working in this repository (`AGENTS.md` points here).
4+
5+
## About
6+
7+
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.
8+
9+
## Commands
10+
11+
### Testing
12+
```bash
13+
composer phpunit # Run PHPUnit test suite
14+
composer phpunit -- --filter=TestClassName # Run a single test class
15+
composer phpunit -- --filter=testMethodName # Run a single test method
16+
composer phpunit:clover # Run tests with code coverage (XML)
17+
composer phpunit:html # Run tests with code coverage (HTML)
18+
```
19+
20+
### Static Analysis & Linting
21+
```bash
22+
composer phpstan # PHPStan at level 8
23+
composer phpcs # PHP compatibility check (7.4+)
24+
vendor/bin/phpcs # WordPress coding standards (uses .phpcs.xml.dist)
25+
```
26+
27+
### Building
28+
```bash
29+
npm run build # Build JS (uglify + less + Gutenberg block)
30+
npm run build-msls-block # Build only the Gutenberg block
31+
```
32+
33+
### E2E Tests
34+
```bash
35+
npx playwright test # Run Playwright tests (against msls.co)
36+
npx playwright test --ui # Run with UI
37+
```
38+
39+
### Local Development Environment
40+
```bash
41+
npx wp-env start # Start WordPress multisite via wp-env (PHP 8.3)
42+
npx wp-env stop
43+
```
44+
45+
## Architecture
46+
47+
### Repository Layout
48+
- `MultisiteLanguageSwitcher.php` — plugin bootstrap
49+
- `includes/` — core PHP classes
50+
- `src/` — JavaScript source components
51+
- `assets/` — CSS, JS, flags, images
52+
- `docs/` — developer reference (API, hooks, snippets)
53+
- `tests/` — PHPUnit and Playwright tests
54+
55+
### Namespace & Autoloading
56+
- 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\`
57+
- PSR-4 (dev): `lloc\MslsTests\` maps to `tests/phpunit/`
58+
- 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()`
59+
- **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
60+
61+
### Key Patterns
62+
- **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
63+
- **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.)
64+
- **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)
65+
- **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
66+
- **Content Import**: `ContentImport/` subsystem handles duplicating content across sites with importers for post fields, meta, terms, attachments, and thumbnails
67+
- **REST API / Quick Create**: `RestApi/` exposes the endpoints behind the editor metabox button and the "Add from Translation" submenu (`Admin\TranslationPicker\`)
68+
69+
### Global API Functions
70+
`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.
71+
72+
### Developer Documentation
73+
`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.
74+
75+
### Test Framework
76+
- PHPUnit 10 with Brain\Monkey for WordPress function mocking
77+
- Patchwork for redefining PHP internals (`filter_input`, `filter_input_array`, `filter_has_var`)
78+
- Base test class: `MslsUnitTestCase` — sets up Monkey, stubs common WP escaping/i18n functions
79+
- Tests mirror the source structure with a `Test` prefix: `includes/Options/Tax/Term.php``tests/phpunit/Options/Tax/TestTerm.php`
80+
81+
## CI
82+
83+
GitHub Actions runs PHPStan, PHPCS, PHPUnit, and Playwright on every pull request.
84+
85+
## Conventions
86+
87+
- WordPress Coding Standards enforced via PHPCS (tabs, Yoda conditions, WordPress function spacing)
88+
- All classes use `declare(strict_types=1)`
89+
- 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' )`)
90+
- Do not modify the plugin header in `MultisiteLanguageSwitcher.php`
91+
- Do not edit `vendor/`, `build/`, `node_modules/`, or language files directly

Changelog.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
## 3.0.0
2+
3+
* 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.
4+
* 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.
5+
* Add filter hooks for the AJAX suggest results of the post and term metaboxes.
6+
* 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`.
7+
* Move the public helper functions into `includes/api.php` and make the `$attr` argument of `msls_get_switcher()` optional.
8+
* Add a PHP-DI container for service construction.
9+
* Documentation: add a developer reference under `docs/` (public API, hooks, snippets, acknowledgements) and refresh the class and package diagrams.
10+
* Fix: check authorization on the destination post during content import, and correct the ContentImporter permission and post type checks.
11+
* Fix: do not fall back to `home_url()` for taxonomy and query archives.
12+
* Fix: broken links on the page for the latest posts.
13+
* Fix: several issues in the blog collection.
14+
* i18n: close gaps in the WP-CLI messages and the Quick Create button title.
15+
* Internal: strict typing throughout, PHPStan level 8 clean, `ABSPATH` guards, Plugin Check and PHPCS findings addressed, wp-env setup for local multisite development.
16+
* Maintenance: numerous dependency updates.
17+
118
## 2.10.1
219
* Fix: Deprecated function warning pointed to non-existent function.
320
* Documentation: update README.md code snippets to reflect new function names.

docs/acknowledgements.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ banner.
1010
Thanks to all translators for their great work.
1111

1212
* German (de_DE) - [Dennis Ploetner](http://lloc.de/)
13-
* Italian (it_IT) - [Antonella Cucinelli](http://www.freely.de/)
13+
* Italian (it_IT) - [Antonella Cucinelli](http://www.freely.de/it/)
1414
* Dutch (nl_NL) - [Alexandra Kleijn](http://www.buurtaal.de/)
15-
* Brazillian Portuguese (pt_BR) - [Victor](http://www.coolweb.com.br/)
15+
* Brazillian Portuguese (pt_BR) - [Coolweb](http://www.coolweb.com.br/)
1616
* Spanish (es_ES) - [Andreas Breitschopp](http://www.ab-weblog.com/en/)
1717
* French (fr_FR) - [Andreas Breitschopp](http://www.ab-tools.com/en/)
1818
* Russian (ru_RU) - [Andrey Vystavkin](http://j4vk.com/)
@@ -36,6 +36,7 @@ Thanks to all translators for their great work.
3636
* Arabic (ar) - Mohamed Elwan
3737
* Norwegian (nb_NO) - Ibrahim Qraiqe
3838
* Bulgarian (bg_BG) - [Vencislav Raev](http://www.catblue.net/)
39+
* Mexican Spanish (es_MX) - [Fernando Mata](https://fernandomata.mx/)
3940

4041
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
4142
existing one, you can [send me](mailto:re@lloc.de) your gettext PO

docs/api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ mentioned only for completeness.
143143

144144
## Deprecated functions
145145

146-
The following pre-2.10.1 names live in `includes/deprectated.php`. Each one
146+
The following pre-2.10.1 names live in `includes/deprecated.php`. Each one
147147
still works but emits a `_deprecated_function()` notice and simply forwards
148148
to its modern `msls_*` replacement. Update calls in your code at your
149149
earliest convenience.

docs/hooks.md

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ switcher renderer for the entire site or per template.
2323

2424
### msls_output_get
2525

26-
Filter applied to the HTML link for every individual language item before it
27-
joins the output array. Use it to wrap, decorate, or replace the per-language
28-
anchor — for example to add a CSS class, swap in a button element, or
29-
append a flag image only on the current language.
26+
Filter that builds the markup for every individual language item before it
27+
joins the output array. It receives the target URL (not the finished anchor),
28+
the `LinkInterface` object, and whether the item points at the current blog —
29+
so the return value has to be the complete HTML for that item. When no
30+
callback is attached, MSLS renders its own default anchor instead. Use it to
31+
wrap, decorate, or replace the per-language link — for example to add a CSS
32+
class, swap in a button element, or append a flag image only on the current
33+
language.
3034

3135
### msls_output_get_tags
3236

@@ -356,6 +360,15 @@ that expects another name shape.
356360

357361
## REST API and Quick Create
358362

363+
### msls_quick_create_capability
364+
365+
Filter on the result of the Quick Create capability check. Alongside the
366+
default decision you get the source post ID (`0` for list-style checks), the
367+
source and target blog IDs, and a `$context` of either `read` (checking access
368+
to the source post) or `create` (checking the right to insert on the target
369+
blog). Use it to let a translator without an account on the source blog mirror
370+
a post into the target blog, or to tighten the default checks.
371+
359372
### msls_quick_create_post_data
360373

361374
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.
444457

445458
### msls_content_import_{type}_importer
446459

447-
Dynamic filter, with `{type}` being one of `post-fields`, `post-meta`,
448-
`terms`, `post-thumbnail`, `attachments`, etc. Returning an `Importer`
449-
instance here forces the factory to use that importer for the corresponding
450-
content type, bypassing the slug-based selection.
460+
Dynamic filter, with `{type}` being the type of one of the five importer
461+
factories: `post-fields`, `post-meta`, `terms`, `post-thumbnail`, or
462+
`attachments`. Returning an `Importer` instance here forces the factory to use
463+
that importer for the corresponding content type, bypassing the slug-based
464+
selection.
451465

452466
### msls_content_import_{type}_importers_map
453467

454-
Dynamic filter, with `{type}` matching one of the factories. It filters the
455-
map of available importer implementations (slug ⇒ class) for that content
456-
type. Use it to register a new importer flavor alongside the built-ins.
468+
Dynamic filter, with `{type}` being one of the five factory types listed
469+
above. It filters the map of available importer implementations
470+
(slug ⇒ class) for that content type. Use it to register a new importer
471+
flavor alongside the built-ins.
457472

458-
### msls_content_import_{slug}_selected
473+
### msls_content_import_{type}_selected
459474

460475
Dynamic filter that picks which importer slug is "selected" for a given
461-
factory. Use it to programmatically switch between competing importer
462-
implementations — for example based on the post type or destination blog.
476+
factory — `{type}` is again one of the five factory types. The default is the
477+
first entry of the factory's importers map. Use it to programmatically switch
478+
between competing importer implementations, for example based on the post type
479+
or destination blog.
463480

464481
### msls_content_import_log_writer
465482

includes/ContentImport/ContentImporter.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ public function import_content( ImportCoordinates $import_coordinates, array $po
308308
* @param array $post_fields
309309
* @param ImportCoordinates $import_coordinates
310310
*
311-
* @since TBD
311+
* @since 2.4.0
312312
*/
313313
$post_fields = apply_filters( 'msls_content_import_data_before_import', $post_fields, $import_coordinates );
314314

@@ -359,7 +359,7 @@ public function import_content( ImportCoordinates $import_coordinates, array $po
359359
* @param ImportLogger $logger
360360
* @param Relations $relations
361361
*
362-
* @since TBD
362+
* @since 2.4.0
363363
*/
364364
do_action( self::MSLS_AFTER_IMPORT_ACTION, $import_coordinates, $logger, $relations ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- constant value is already prefixed with "msls_".
365365

includes/ContentImport/Importers/ImportersBaseFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public function make( ImportCoordinates $import_coordinates ) {
5454
* @param array $map A map of importers in the shape [ <importer-slug> => <importer-class> ]
5555
* @param ImportCoordinates $import_coordinates
5656
*
57-
* @since TBD
57+
* @since 2.4.0
5858
*/
5959
$map = apply_filters( "msls_content_import_{$type}_importers_map", $this->importers_map, $import_coordinates );
6060

includes/RestApi/RestApi.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ private static function apply_capability_filter( bool $default_cap, int $source_
252252
* @param int $target_blog_id Target blog ID.
253253
* @param string $context 'read' when checking the source, 'create' when checking the target.
254254
*
255-
* @since TBD
255+
* @since 3.0.0
256256
*/
257257
return (bool) apply_filters(
258258
'msls_quick_create_capability',
@@ -299,7 +299,7 @@ public function create_translation( \WP_REST_Request $request ) {
299299
* @param int $source_blog_id The source blog ID.
300300
* @param int $target_blog_id The target blog ID.
301301
*
302-
* @since TBD
302+
* @since 3.0.0
303303
*/
304304
$post_data = apply_filters( 'msls_quick_create_post_data', $post_data, $source_post, $source_blog_id, $target_blog_id );
305305

@@ -333,7 +333,7 @@ public function create_translation( \WP_REST_Request $request ) {
333333
* @param int $source_blog_id The source blog ID.
334334
* @param int $target_blog_id The target blog ID.
335335
*
336-
* @since TBD
336+
* @since 3.0.0
337337
*/
338338
do_action( 'msls_quick_create_after_insert', $new_post_id, $source_post, $source_blog_id, $target_blog_id );
339339

@@ -358,7 +358,7 @@ public function create_translation( \WP_REST_Request $request ) {
358358
* @param int $source_blog_id The source blog ID.
359359
* @param int $target_blog_id The target blog ID.
360360
*
361-
* @since TBD
361+
* @since 3.0.0
362362
*/
363363
$response_data = apply_filters(
364364
'msls_quick_create_response',
@@ -440,7 +440,7 @@ public function list_untranslated_posts( \WP_REST_Request $request ) {
440440
* @param int $target_blog_id Target blog ID.
441441
* @param string $post_type Post type queried.
442442
*
443-
* @since TBD
443+
* @since 3.0.0
444444
*/
445445
$items = apply_filters( 'msls_untranslated_posts', $items, $source_blog_id, $target_blog_id, $post_type );
446446

@@ -549,7 +549,7 @@ protected function prepare_taxonomies(
549549
* @param int $source_blog_id The source blog ID.
550550
* @param int $target_blog_id The target blog ID.
551551
*
552-
* @since TBD
552+
* @since 3.0.0
553553
*/
554554
$post_data['_msls_tax_input'] = apply_filters(
555555
'msls_quick_create_tax_input',

0 commit comments

Comments
 (0)