This file is the single source of truth for coding agents working in this repository (AGENTS.md points here).
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.
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)composer phpstan # PHPStan at level 8
composer phpcs # PHP compatibility check (7.4+)
vendor/bin/phpcs # WordPress coding standards (uses .phpcs.xml.dist)npm run build # Build JS (uglify + less + Gutenberg block)
npm run build-msls-block # Build only the Gutenberg blocknpx wp-env start # Required first: the local suite runs against wp-env
npm run playwright:local # Admin + frontend specs (skips visual)
npm run playwright:visual # Visual specs inside the Playwright Linux image
npm run playwright:docker # Whole local suite inside that image
npm run playwright:update-snapshots # Regenerate visual baselines
npm run playwright:live # Only specs/live, against msls.co (no wp-env needed)
npx playwright test --ui # Run with UIVisual baselines are committed and only pixel-stable when generated inside the container —
always use the :visual / :update-snapshots scripts, never a bare npx playwright test
for them.
The live project is a read-only smoke test against an already-running installation.
baseURL comes from MSLS_LIVE_URL (default https://msls.co), so
MSLS_LIVE_URL=https://staging.example.com npm run playwright:live retargets it. The
script also sets MSLS_LIVE_ONLY=1, which makes globalSetup return before it touches
wp-env — running npx playwright test --project=live without it attempts the local
seeding and fails. The target has to serve a public /testpage carrying the switcher
markup the specs assert on. See docs/e2e-testing.md for the full contract.
npx wp-env start # Start WordPress multisite via wp-env (PHP 8.3)
npx wp-env stop
npx wp-env reset tests # Wipe the tests database (fresh-install check)
npx wp-env run cli wp plugin activate multisite-language-switcher --networkThe plugin reaches the container through mappings in .wp-env.json, not through
plugins — mounting it both ways created a duplicate Multisite-Language-Switcher
plugin directory and made wp-env start fail. Consequence: wp-env does not auto-activate
it, so activate it once per fresh development environment with the command above. The
tests environment is network-activated by the Playwright global setup.
MultisiteLanguageSwitcher.php— plugin bootstrapincludes/— core PHP classessrc/— JavaScript source componentsassets/— CSS, JS, flags, imagesdocs/— developer reference (API, hooks, snippets)tests/— PHPUnit and Playwright tests
- PSR-4:
lloc\Msls\maps toincludes/, 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 totests/phpunit/ - Plugin bootstrap:
MultisiteLanguageSwitcher.php— defines constants, requiresvendor/autoload.phpplusincludes/aliases.php,includes/deprecated.phpandincludes/api.phpat file-load time, then callslloc\Msls\Plugin::init()andlloc\Msls\Cli\Cli::init()onplugins_loaded. Do not move those requires into the hook: add-ons may load before us, and they need the aliases and themsls_*()functions to exist the moment the plugin file is included - Backwards-compatibility aliases:
lloc\Msls\Compat\Aliases::MAP(includes/Compat/Aliases.php) maps the ~60 pre-3.0 flat class names (MslsOptions,MslsLink,MslsPlugin, …) to their namespaced replacements.::register()— invoked from the thinincludes/aliases.php— creates them withclass_alias()eagerly, plus an autoloader for the handful in::LAZY_ONLY. Do not make them lazy across the board: PHP resolves the class named in a parameter/return/property type withZEND_FETCH_CLASS_NO_AUTOLOAD, so an alias created on demand never gets its chance and the call fatals with aTypeError(MslsMenu declaresget_msls_output(): lloc\Msls\MslsOutput).LAZY_ONLYis limited to names that never shipped before 3.0, so nothing can be holding them. Write new code against the namespaced names; the aliases exist only for third-party consumers - PHP-DI:
lloc\Msls\Container::get()builds the container fromconfig.phpon first use and caches it.config.phpis still empty — nothing is injected through it yet
- Registry/Singleton:
Registry\Instanceis the base class providing the::instance()static accessor (backed byRegistry\Registry);Registry\GetSetextends 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, extendsGetSet) →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\Linkbase class with variants (Link\TextOnly,Link\ImageOnly,Link\TextImage) — selected by the display index 0–3 fromLink\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\)
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.
docs/ holds the reference material: api.md (public API functions), hooks.md (every action and filter), snippets.md (integration recipes), e2e-testing.md (the Playwright local and live projects), acknowledgements.md (credits and translators). Keep these in sync when adding or renaming a hook or an API function.
- 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
Testprefix:includes/Options/Tax/Term.php→tests/phpunit/Options/Tax/TestTerm.php
GitHub Actions workflows:
test.yml— PHPUnit on every push (plus Codecov upload onmaster)e2e.yml— the Playwrightlocalproject (admin + frontend specs) on pull requests and pushes tomaster; visual specs and theliveproject are not run in CIplugin-check.yml— WordPress.org Plugin Check on pull requests andmasterdeploy.yml— WordPress.org deploy on tags
PHPStan and PHPCS are not wired into CI yet — run them locally via composer qa.
- WordPress Coding Standards enforced via PHPCS (tabs, Yoda conditions, WordPress function spacing)
- All classes use
declare(strict_types=1) - Text domain:
multisite-language-switchereverywhere — in the plugin header and in every__()/esc_html__()call. Do not usemslsas 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