Skip to content

Latest commit

 

History

History
123 lines (100 loc) · 8.69 KB

File metadata and controls

123 lines (100 loc) · 8.69 KB

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

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

composer phpstan                    # PHPStan at level 8
composer phpcs                      # PHP compatibility check (7.4+)
vendor/bin/phpcs                    # WordPress coding standards (uses .phpcs.xml.dist)

Building

npm run build                       # Build JS (uglify + less + Gutenberg block)
npm run build-msls-block            # Build only the Gutenberg block

E2E Tests

npx 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 UI

Visual 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.

Local Development Environment

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 --network

The 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.

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, requires vendor/autoload.php plus includes/aliases.php, includes/deprecated.php and includes/api.php at file-load time, then calls lloc\Msls\Plugin::init() and lloc\Msls\Cli\Cli::init() on plugins_loaded. Do not move those requires into the hook: add-ons may load before us, and they need the aliases and the msls_*() 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 thin includes/aliases.php — creates them with class_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 with ZEND_FETCH_CLASS_NO_AUTOLOAD, so an alias created on demand never gets its chance and the call fatals with a TypeError (MslsMenu declares get_msls_output(): lloc\Msls\MslsOutput). LAZY_ONLY is 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 from config.php on first use and caches it. config.php is still empty — nothing is injected through it yet

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), 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.

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 workflows:

  • test.yml — PHPUnit on every push (plus Codecov upload on master)
  • e2e.yml — the Playwright local project (admin + frontend specs) on pull requests and pushes to master; visual specs and the live project are not run in CI
  • plugin-check.yml — WordPress.org Plugin Check on pull requests and master
  • deploy.yml — WordPress.org deploy on tags

PHPStan and PHPCS are not wired into CI yet — run them locally via composer qa.

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