CodeOn Multilingual
Lightweight multilingual plugin for WordPress and WooCommerce. WPML-compatible, fraction of the weight.
by Samsiani / CodeOn · github.com/samsiani/codeon-multilingual · website
Install
The author publishes release zips, so WP-CLI can install straight from GitHub:
wp plugin install https://github.com/samsiani/codeon-multilingual/releases/download/v0.9.3/codeon-multilingual.zipReadme
CodeOn Multilingual
A lightweight multilingual plugin for WordPress and WooCommerce. WPML-compatible feature surface at a fraction of the weight.
Why this exists
WPML and Polylang work, but they carry a lot of 2009-era baggage: 17+ DB tables, polymorphic indexes that MySQL plans poorly, React-heavy admin bundles, translation-workflow surface most sites never use. We deliver the same outcomes at ~10% of the disk weight and ~10–20% of the per-request query/memory cost.
| Dimension | CodeOn Multilingual | Polylang | WPML |
|---|---|---|---|
| PHP code (LOC) | ~10.9 k | ~30–50 k | ~100 k+ across the suite |
| Release ZIP | ~700 KB (incl. 60 SVG flags + catalog) | ~3 MB | ~50–150 MB |
| DB tables added | 5 | 2 + reused taxonomy | 17+ |
| Frontend JS shipped by default | ~1.4 KB (dropdown toggle) | ~30 KB | ~200 KB+ |
| Admin JS bundle | ~6 KB vanilla | ~120 KB jQuery+UI | 400 KB+ React |
| Extra queries per WP_Query | 1 LEFT JOIN | 2–3 (taxonomy joins) | 3–7 |
gettext hot path overhead |
1 isset() per call against a request-static map | filter per call | filter + DB hit |
| Memory per request | <1.5 MB | 3–5 MB | 8–15 MB |
Status
v0.9.2 — live-tested on artcase.ge. The locked v0.1.0 MVP scope is shipped, plus migration tooling, inline string editor, scan-based discovery, WP 6.5+ native .l10n.php translation path, the WPML compatibility shim (13 API surfaces — themes/plugins written against WPML's public API run unmodified), a full WP-CLI command surface for ops/CI/benchmark workflows, a first-run setup wizard with a bundled 66-language catalog, bundled SVG flags (60 countries — Windows-safe vs emoji-only rendering), per-language compiled-map cache, a hardened WooCommerce translation surface (product field locking on translations, automatic shop-page mapping per language, cart/order/email language handling, payment/shipping/coupon/status/notice/download label translation, translated product terms/variation attribute slugs, cart items follow current language with fallback to original, WC attribute label translation), a dedicated menu translation flow, per-language columns on posts/taxonomy admin lists, WPML rollback snapshots, a production Health screen, and blocking PHPCS/PHPStan/unit/package/WordPress+WooCommerce integration gates.
See ROADMAP.md for what's built, what's missing, and what's next.
Features at a glance
Routing
- Subdirectory URL strategy (
/ka/,/en/), virtual prefix, no flush_rewrite_rules() needed - Single
LEFT JOINinjection on everyWP_QueryandWP_Term_Query - Per-language locale switching (
<html lang>,.mobundle selection, hreflang)
Content
- Post / Page / CPT / Attachment translation via deep
post_metaclone - Taxonomy term translation with hierarchical parent mapping
- Slug uniqueness scoped per (post_type, post_parent, language)
- Auto-tag new posts on
save_post - Cron-driven backfill for legacy content
WooCommerce
- Product translation with full variation auto-clone
- 22 props synced across the translation group: price, stock, manage_stock, backorders, sold_individually, low_stock_amount, sale dates, dimensions, weight, tax_class, tax_status, downloadable, virtual, SKU, GTIN/UPC/EAN/ISBN, shipping class
- Field-lock UI on translations (WPML-style): pricing, SKU, GTIN, stock controls, dimensions, shipping class, tax, virtual/downloadable, and the entire Attributes panel render disabled + read-only on translated products; banner reads "You're editing a translation of — edit the original to change these"
- Shop-page mapping per language: on
/en/cart/,wc_get_page_id('cart')returns the English cart page sois_cart()matches and the cart template fires - Cart items follow current language: line items, mini-cart, checkout review, order-received page, and emails swap to the translation; items without a translation stay in their original language
- Attribute labels translatable (Color / Size / ფერი → translate from Multilingual → Strings; filter
woocommerce_attribute_labelswaps per language) - Attribute terms translatable (each
pa_*taxonomy has the same per-language column UI as categories) - WC's "duplicate SKU" validator suppresses errors when the conflict is just another sibling in the same translation group
- Translated WP pages without a sibling in the current language fall back to the source page rather than 404
- Group-keyed lock prevents recursion during sibling sync
Strings
- Hash-keyed compiled map in wp_cache, keyed per language so a request that touches multiple locales (e.g. WP boot's early
__()calls + later router lang switch) never bleeds one language's translations into another - Translations into the default language apply correctly (the source code is in English; the site default is Georgian; the Georgian translation of an English source string IS the right thing to render)
- Opt-in WP 6.5+ native
.l10n.phpwriter (atomic + opcache-aware) - File-system scanner with regex for
__,_e,_x,_ex,esc_* - Inline popup editor (vanilla JS, click-outside-saves)
- Per-string source-language detection + badge
- Plugin integration API:
StringTranslator::register_source()+lookup_translation()for sources that bypass gettext (e.g. WooCommerce attribute labels, gateway titles, email subjects). One hash lookup against the request-static compiled map on the hot path.
Frontend
- Shortcode
[cml_language_switcher]+ classic widget + auto-floating switcher + nav-menu item + Gutenberg block — five drop-in surfaces wrapping the sameLanguageSwitcher::render() - Nav-menu item: side meta-box on Appearance → Menus inserts a single placeholder that auto-expands at render time, with per-item display (names / flags / both) and layout (inline / dropdown) settings
- Gutenberg block (
codeon-multilingual/switcher): server-rendered,ServerSideRenderpreview in the editor, style + show-flag/native/code attributes, wide/full alignment + spacing controls - Custom button/menu dropdown with bundled SVG flags (60 countries, MIT-licensed from lipis/flag-icons; Windows desktop without flag-emoji glyphs renders properly now)
- Dropdown menu auto-sizes to the widest item, opens upward when anchored at the bottom of the viewport, and locks to the floating-switcher box width with a measure-and-pin JS pass that's stable across repeat clicks
- Hreflang link tags +
x-default <html lang>+dirrewriting per current view- WP-core sitemap participation (all-language enumeration)
Admin
- First-run setup wizard at
admin.php?page=cml-setup— 4 steps (welcome → default language → secondary languages → done), 66-language catalog, search-as-you-type, SVG flags, auto-fills locale/native/RTL when a code is recognised - Defensive redirect:
admin-post.php?page=cml-setup&step=N302s to the canonicaladmin.php?...so stale bookmarks don't white-screen - Languages admin list shows SVG flag per row + "Run setup wizard" page-title action
- "Add Language" form auto-fills locale, English name, native name, flag, RTL from the catalog as the admin types a code
- Per-post-type and per-taxonomy language quick-link row above each list table
- Per-language columns — one narrow column per active language on every translatable posts list AND taxonomy list. Header shows flag + uppercase code (
🇬🇪 KA / 🇷🇺 RU / 🇺🇸 EN); each cell renders the flag for the row's source language and a green ✓ / grey + for every other (linked to the sibling edit screen or "create translation" respectively) - Menu translation flow: Multilingual → Menus lists every nav menu with per-language "Sync" actions. Untranslated items copy from source so translated menus are never half-empty
- Translation slug derived from translated title: drafts keep
post_nameempty until publish, so a Georgianვაშლი→ translated asApple→ published with/apple/, not/ვაშლი/ - Admin bar language indicator reflects the edited post's language (not the admin user's) on edit screens; sub-menu jumps directly to sibling edit screens; "Add translation" always creates from the group source
- "Permalink:" preview on the translation edit screen now reflects the post's own language URL
- Translation meta box distinguishes the source ("Edit X (original)") from translations ("Edit X translation")
- Cookie-persisted language selection
- WPML data migration tool (5-statement SQL importer)
- Polylang migration tool for languages, post mappings, term mappings, and dynamic strings
REST
?lang=parameter on everyshow_in_restendpointPOST /cml/v1/strings/<id>/translationsfor inline editor
WPML compatibility
icl_object_id,icl_get_languages,icl_get_default_language,icl_get_current_languagefunctionswpml_object_id,wpml_current_language,wpml_default_language,wpml_active_languages,wpml_post_language_details,wpml_element_has_translations,wpml_translate_single_stringfilterswpml_register_single_string,wpml_switch_languageactions- Astra, GeneratePress, WoodMart, Elementor, YITH, Woo extensions work without code changes
WP-CLI
wp cml language list / create / activate / deactivate / set-default / deletewp cml translate post <id> --to=<lang>/wp cml translate term <id> --tax=<tax> --to=<lang>wp cml strings scan [--theme=<slug>] [--plugin=<slug>] [--all]wp cml strings export --lang=<code> [--format=po|json] [--domain=<d>] [--output=<file>]wp cml strings import <file> [--format=auto|po|json] [--lang=<code>]wp cml migrate wpml [--dry-run] --snapshot=<file>wp cml migrate polylang [--dry-run] --snapshot=<file>wp cml migrate sourceswp cml migrate export --output=<file>wp cml migrate rollback <file> [--dry-run|--confirm-rollback]wp cml backfill run [--all]/status/resetwp cml health summary/report [--failed-only]wp cml health repair [--dry-run|--apply] [--scope=all|orphaned-post-rows|orphaned-term-rows|orphaned-string-rows|missing-post-rows|missing-term-rows|unknown-source-languages]wp cml benchmark report/seed --yes/cleanup --dry-run|--yes— strings plus realWP_Queryproduct/variation collection workloads in the selected language, with guarded fixture cleanup
v0.9.x hardening
- WPML/Polylang migration preflight is stricter about default-language and source-language conflicts.
- Admin WPML/Polylang imports create a source-tagged rollback snapshot before writes; WP-CLI imports remain snapshot-gated.
- WooCommerce product relationship IDs and variation option display labels remap to current-language siblings where possible.
- WooCommerce Store API language state is scoped to Store API dispatches, including cart item product-object swaps and notice payload translation.
- Yoast SEO and Rank Math breadcrumb/schema/canonical/sitemap surfaces localize URLs, post IDs, and term IDs through CodeOn translation groups.
- Builder/template library CPTs are included in the default translatable post type set when present.
Requirements
- PHP 8.1+
- WordPress 6.5+
- WooCommerce 8.0+ (optional, only for product sync)
Install
Production (auto-updates)
Download the latest ZIP from Releases and install via Plugins → Add New → Upload. Plugin Update Checker keeps it current as new tags are pushed.
Development
git clone https://github.com/Samsiani/codeon-multilingual.git
cd codeon-multilingual
composer install
ln -s "$(pwd)" /path/to/wp-content/plugins/codeon-multilingual
Then activate in Plugins.
See DEVELOPMENT.md for the full dev workflow.
Quick start
- Activate the plugin — the setup wizard opens automatically on first run, walking you through default + secondary language selection from a bundled catalog of 66 curated languages.
- (After the wizard) Edit any post — use the Translations meta box to create translations.
- Visit
/{lang}/on the front-end to see the translated site. - Multilingual → Settings — tune the floating switcher and the optional native
.l10n.phptranslation path.
If you skipped the wizard, "Run setup wizard" stays available as a link on the Languages page.
Documentation
| Doc | Purpose |
|---|---|
README.md |
This file — overview and quick start |
ARCHITECTURE.md |
Deep dive: modules, design decisions, hot paths, schema, request lifecycle |
ROADMAP.md |
Current state, gap analysis, prioritised next steps |
CHANGELOG.md |
Per-release notes |
DEVELOPMENT.md |
Dev environment, tests, release workflow |
License
GPL-2.0-or-later. See plugin header.
Read the full README on GitHub →
Releases
| Tag | Published | Asset | Downloads |
|---|---|---|---|
| v0.9.3 | Aug 31, 2026 | codeon-multilingual.zip | 1 |
| v0.9.2 | May 19, 2026 | codeon-multilingual.zip | 1 |
| v0.9.1 | May 19, 2026 | codeon-multilingual.zip | 2 |
| v0.9.0 | May 18, 2026 | codeon-multilingual.zip | 0 |
| v0.8.0 | May 18, 2026 | codeon-multilingual.zip | 0 |
| v0.7.37 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.36 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.35 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.34 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.33 | May 14, 2026 | codeon-multilingual.zip | 4 |
| v0.7.32 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.31 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.30 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.29 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.28 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.27 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.26 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.25 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.24 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.23 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.22 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.21 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.20 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.19 | May 14, 2026 | codeon-multilingual.zip | 0 |
| v0.7.18 | May 14, 2026 | codeon-multilingual.zip | 0 |