WP Manifestindependent plugin directory
manifest / i18n / wp-loc

WP-LOC

Lightweight multilanguage plugin for WordPress

★ 2stars

Install

Download source zip No release zip yet

This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/vitaliikaplia/wp-loc/archive/refs/heads/master.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

Lightweight multilingual plugin for WordPress.

Features

  • Language management — Multilingual > Languages page with drag-and-drop ordering (auto-saves via AJAX)
  • Automatic language detection — install a language in WP General Settings, it auto-appears in WP-LOC. Delete from WP-LOC — removes language files too.
  • Translatable post type translations — auto-create translation drafts for enabled post types, translation metabox in editor with per-language + button for on-demand creation
  • AI-assisted names — translate enabled post type titles and term names from admin list actions, Gutenberg/classic editor title controls, and term edit controls through the selected WordPress AI connector
  • Translated posts-page editor parity — localized page_for_posts values let WordPress recognize every translated posts page in both Classic Editor and Gutenberg, including the native latest-posts notice and editor restrictions
  • Post translation lifecycle — shared status/trash state can stay synchronized across a translation group, and permanently deleting any translated post permanently deletes its sibling translations and translation rows
  • Bidirectional post taxonomy sync — when a translated post changes its multilingual categories/tags/custom taxonomies, sibling posts receive the mapped term translations in their own language and stale foreign-language relationships are removed; REST/Gutenberg set_object_terms updates are handled after save_post
  • Taxonomy/term translations — migration-compatible term translation groups for category, post_tag, and selected custom taxonomies
  • Term translation UI — translation column in term lists, translation panel on term edit screens, and per-language + buttons for on-demand term translation creation
  • Automatic term translation creation — creating a term can auto-create sibling translations in all active languages
  • Hierarchical taxonomy sync — translated parent terms are mapped automatically when creating/editing child terms
  • Same slug in different languages — term slugs are unique per language, not globally
  • Cascade delete for term translations — deleting any translated term deletes the whole translation group
  • Protected default category group — default category and all its translations cannot be deleted
  • Multilingual nav menus — translated menu groups, translated menu items, language-aware menu locations, auto-created menu translations, and cascade deletion for menu translation groups
  • Tools page — Multilingual > Tools with tabbed utilities for WP Menus Sync and Config Migration
  • WP Menus Sync — AJAX preview/apply for syncing menu structure from the default language to secondary languages
  • AI-assisted custom menu links — optional AI translation for custom nav menu items during menu sync, while preserving URLs and other menu item settings, with safe fallback when the AI provider refuses a short-field translation
  • Config Migration tool — detects legacy multilingual config files (wpml-config.xml), reads only translatable post types and taxonomies, generates lightweight wp-loc-config.xml, and can remove theme-level legacy config files
  • Database Optimization Wizard — appears in admin after activation, scans multilingual data left by another plugin, adopts compatible translation links, imports languages, detects translated post types/taxonomies/options, and removes obsolete service data after confirmation
  • Manual language mapping during optimization — the wizard auto-matches detected languages through a built-in registry, shows match confidence, and lets admins override the target WP-LOC language before applying cleanup
  • Language registry — central locale/code/slug/name/flag normalization for common WordPress and multilingual-plugin language codes, including aliases like uk → ua and legacy iw → he
  • Separate URL slugs and compatibility codes — languages can use URL slugs like ua while compatible database/API language codes remain uk
  • Non-translatable post types — work correctly with language URL prefixes (shared content across languages)
  • Translatable post type detection — if compatible translation rows already exist, WP-LOC detects translated custom post types and taxonomies and merges them into runtime settings even when older saved settings are incomplete
  • Detected translation visibility — the settings screen shows the number of existing compatible translation records beside each detected post type, so administrators can see why it remains multilingual
  • Selectable content types — public post types/taxonomies and custom non-public registered objects can be enabled from settings, while internal WordPress objects and specially handled objects stay excluded
  • Frontend/admin query filtering — translatable posts are filtered by the current language for main, secondary, AJAX, REST, and Gutenberg preview WP_Query calls when filters are not suppressed
  • Language-scoped link picker — WordPress's native Insert/edit Link dialog only lists translatable content from the language currently being edited
  • Frontend AJAX language context — standard admin-ajax.php handlers inherit the current frontend language through request parameters, the referring URL, and compatible cookies, in that order, so AJAX started from a page served by a full-page cache still answers in the language on screen
  • Runtime language switching — WPML-style wpml_switch_language / $sitepress->switch_lang() calls temporarily switch WP-LOC's language and WordPress locale, so background handlers and transactional email flows can render content in a user's preferred language
  • URL structure — the default language has no prefix; additional languages use /{slug}/page-slug/, for example /en/page-slug/
  • Migrated default language — the migration wizard preserves the legacy multilingual default language for no-prefix URLs
  • Admin language switcher — in the admin bar with flags, cookie-based
  • Frontend language switcher — wp_loc_get_lang_switcher(), wp_loc_get_language_switcher_html(), wp_loc_the_language_switcher() with translated post, custom post type, taxonomy, author, search, date, paginated, query-filtered, and archive URLs
  • SEO — frontend hreflang alternate tags for translated singular, front page, posts page, and taxonomy contexts; canonical URL fallback when no SEO plugin outputs one; proper <html lang="">
  • Yoast SEO compatibility — localized wpseo_titles / wpseo_social / wpseo_rss options, translated primary category resolution, copied Yoast term SEO meta for translated terms, multilingual sitemap alternate links, stripped category-base compatibility, and Yoast indexable invalidation after multilingual updates
  • Localized options — blogname, blogdescription, page_on_front, page_for_posts, plus options registered through wp_loc_multilingual_options or compatible wpml_multilingual_options, per language and with localized front page / posts page routing
  • Custom settings page support — localized options are displayed and saved correctly on WordPress settings pages, including custom submenu pages under Settings that register option names dynamically
  • AI settings — detect configured WordPress AI provider connectors, choose an available provider and text-generation model, and enable AI translation for custom menu links during menu sync
  • Translation workflow settings — control automatic creation of post, term, and menu translations and enable multilingual behavior for public or custom non-public post types and taxonomies from Multilingual > Settings > Content Translation
  • Sync policy settings — control taxonomy sync, featured image sync, and shared post-attribute sync for translation groups
  • Switcher behavior settings — control whether the frontend switcher shows flags and names, hides the current language, hides untranslated targets, or falls back to language home URLs
  • Integration toggles — enable or disable ACF compatibility, Yoast compatibility, and Yoast sitemap alternate links from Multilingual > Settings > Integrations
  • Third-party compatibility — icl_object_id(), $sitepress, ICL_LANGUAGE_CODE, common multilingual filters
  • Multilingual media — uploads receive linked attachment records for all active languages while sharing one physical file; media queries, featured images, frontend image resolution, and deletion are language-aware
  • ACF integration — ACFML-like field/group translation config for DB, local JSON, and PHP-registered field groups, plus language-aware options_{lang} routing for options pages
  • ACF field translation modes — shared, copy_once, translatable, and editable shared-value none behavior for multilingual field workflows
  • ACF media/relation mapping — translated attachment, post, term, and nav menu IDs are resolved per language for fields like image, file, gallery, post_object, page_link, relationship, taxonomy, and nav_menu
  • ACF container field support — multilingual behavior for group, repeater, flexible_content, and clone fields across options pages, posts/pages, and term edit screens
  • ACF nav_menu field support — translated menu values resolve to the correct menu in the current language context
  • Yoast Duplicate Post integration — cloning a translatable post also clones its whole translation group; the copies are duplicated through Duplicate Post's own engine and linked together as a new translation group, while Rewrite & Republish is left untouched
  • Timber integration — Twig functions wp_loc_language_switcher() and wp_loc_languages()
  • Activation safety — on activation, WP-LOC deactivates known conflicting multilingual add-ons instead of deleting them
  • GitHub updates — native WordPress plugin updates from the public GitHub repository by comparing the remote wp-loc.php Version: header on the configured branch
  • Ukrainian slug — uk locale → ua URL slug out of the box

Requirements

  • WordPress 6.0+
  • PHP 8.1+
  • WordPress 7.0+ and a configured AI provider connector for AI-assisted translation features

Development

  • The project uses prepros.config, but routine asset builds are handled automatically by the Mini Prepros watcher in PhpStorm; the desktop Prepros application does not need to be opened
  • Edit assets/scss/admin.scss, its imported partials, assets/js/admin.js, or prepended JavaScript sources only; Mini Prepros rebuilds assets/css/admin.min.css and assets/js/admin.min.js
  • Never edit the generated .min.css or .min.js files manually; after source changes, verify that the watcher updated both configured outputs

Installation

  1. Upload wp-loc folder to /wp-content/plugins/
  2. Activate the plugin
  3. If the Database Optimization Wizard appears, review the scan, adjust language mapping if needed, and apply or dismiss it
  4. Go to WP General Settings and install any additional languages you need — they auto-appear in Multilingual > Languages
  5. Configure language slugs, display names and ordering in Multilingual > Languages
  6. Select or review translatable post types and taxonomies in Multilingual > Settings
  7. Configure Multilingual > Settings tabs for content workflow, switcher behavior, integrations, and AI provider settings
  8. Use Multilingual > Tools for WP Menus Sync and Config Migration

Usage

GitHub updates

  • WP-LOC uses the standard WordPress plugin update flow, so update checks, update notices, AJAX updates, and disabled-update hooks behave like native plugin updates
  • The updater reads Version: from wp-loc.php on the configured GitHub branch and compares it with the installed WP_LOC_VERSION
  • Update checks are cached in a site transient and do not ping GitHub on every admin request
  • GitHub Releases are not required; the update package is the configured branch ZIP archive
  • To publish an update, keep the plugin header Version:, WP_LOC_VERSION, and the Project-Id-Version in languages/wp-loc.pot synchronized, then push the branch

In PHP templates

// Get current language
$lang = wp_loc_get_current_lang(); // 'ua', 'en', 'ru'

// Get language switcher
$switcher = wp_loc_get_lang_switcher();
foreach ( $switcher as $lang ) {
    echo '<a href="' . $lang['url'] . '" class="' . ($lang['active'] ? 'active' : '') . '">';
    echo '<img src="' . $lang['flag'] . '" /> ' . $lang['name'];
    echo '</a>';
}

// Or render ready-to-use markup:
wp_loc_the_language_switcher();

// Get translated term ID
$translated_term_id = icl_object_id( $term_id, 'category', true, 'en' );

// Get translated post ID
$translated_id = icl_object_id( $post_id, 'page', true, 'en' );

// Register a multilingual option
do_action( 'wp_loc_multilingual_options', 'my_custom_option' );

// Compatible registration used by many existing themes/plugins
do_action( 'wpml_multilingual_options', 'my_custom_option' );

// Temporarily switch language for a background/transactional flow
do_action( 'wpml_switch_language', 'ru' );
$subject = get_option( 'my_custom_option' ); // reads my_custom_option_ru when available
do_action( 'wpml_switch_language', 'uk' ); // restore previous/default context when done

Content editor and lifecycle

  • Post list row actions and Gutenberg/classic editor title controls can translate titles for every post type enabled in Multilingual > Settings > Content Translation
  • Term list row actions and term edit controls can translate term names for taxonomies enabled in Multilingual > Settings > Content Translation
  • WordPress's native Insert/edit Link modal is scoped to the language of the post currently being edited, including when ACF opens that native dialog
  • Every translated page selected through localized page_for_posts is treated as the posts page by both Classic Editor and Gutenberg, so WordPress shows its native latest-posts notice and applies the normal editor restrictions
  • When shared post attributes are synchronized, changing a translation's status also updates its siblings; permanently deleting any post in a translatable group removes all sibling posts and their icl_translations rows
  • Term relationships sync across a post's translation group, but an empty source term set is never mirrored onto siblings — a save that carries no terms for a taxonomy leaves every translation's terms untouched
  • The block editor's category/tag panels show the term tree in the language of the post being edited (REST taxonomy collections are scoped by the editor's post context, matching the classic editor's behavior)

Multilingual menus

  • Create the source menu in the default language
  • WP-LOC auto-creates sibling menus in the other active languages
  • Menu locations are assigned from the default-language menu and resolved automatically per language on the frontend
  • Use Multilingual > Tools > WP Menus Sync to sync structure/order/options from the default-language menu to translated menus
  • If Try to translate custom nav menu links with AI during menu sync is enabled in Multilingual > Settings > Content Translation, custom menu links are translated with the selected AI provider and model during sync; otherwise they are duplicated 1:1 with the same title, URL, and item settings
  • If an AI provider returns a refusal or unusable short-text response for a custom menu link field, WP-LOC keeps the original field value instead of saving the refusal text into the translated menu item
  • Automatic menu creation can be disabled from Multilingual > Settings > Content Translation if you prefer to create translated menus manually

Multilingual media

  • A newly uploaded attachment is registered in the current language and receives linked attachment posts for the other active languages; all records share the same physical file and generated image metadata
  • Media Library list/grid views and media modals are filtered to the current admin language, while unregistered legacy attachments remain available
  • Featured-image assignment and removal can be synchronized across post translations, resolving each sibling to the corresponding translated attachment when one exists
  • Frontend image lookup resolves a registered attachment to the current-language record
  • Permanently deleting any attachment translation deletes the full attachment translation group without deleting the shared physical file more than once

AI integration

  • Install and configure an official AI provider from Settings > Connectors in WordPress 7.0 or newer; WP-LOC does not store provider API keys
  • In Multilingual > Settings > AI, choose one of the connected providers and one of its available text-generation models
  • Provider and model availability comes from the WordPress AI Client registry, so the choices follow the installed provider connectors instead of a hardcoded model list
  • The selected provider and model are used for AI-assisted title, term-name, and custom menu-link translations

Config migration

  • In Multilingual > Tools > Config Migration, WP-LOC scans the active theme, parent theme, and active plugins for legacy multilingual config files such as wpml-config.xml
  • WP-LOC reads only custom-types and taxonomies from supported legacy config files; other config sections are ignored on purpose
  • You can generate a lightweight wp-loc-config.xml from the current WP-LOC settings
  • You can also generate wp-loc-config.xml from a detected legacy config source
  • Theme-level legacy config files can be removed from the same screen after migration; plugin-level files are shown as read-only

Database optimization wizard

  • The wizard opens automatically for admins after activation while its status is pending
  • Closing the modal with the X only hides it for the current page load; dismissing optimization stores that choice and stops the automatic modal
  • A plugin-row Wizard action remains available while the wizard has not been completed, so admins can return to it after dismissing
  • The scan summarizes compatible translation links, detected content types, taxonomies, media, menus, localized options, language records, and removable service data
  • Detected languages are normalized through WP_LOC_Language_Registry; the wizard shows match confidence and lets admins manually map each detected source language to a WP-LOC target language
  • Imported languages preserve compatible language codes and detected switcher display names where available, so URL slugs and database/API codes do not have to be identical
  • Applying optimization imports or updates WP-LOC languages, adopts compatible translation links, imports detected translatable post types/taxonomies into settings, cleans obsolete options/meta/tables, and marks the wizard as completed
  • The apply flow requires confirmation because removed service data is not restored by WP-LOC

ACF options pages

  • shared fields stay on the base ACF options post ID (options)
  • Saving translated options, posts, or terms ignores read-only shared fields so disabled ACF containers cannot overwrite source-language values
  • translatable fields are routed through language-aware ACF options post IDs like options_en / options_ru
  • copy_once container fields inherit from the source language until the translated options page stores its own value
  • Both get_field( 'field_name', 'options' ) and get_fields( 'options' ) resolve translated values in the current language context
  • nav_menu ACF fields resolve to the translated menu for the current language

Localized options and background flows

  • Options registered through wp_loc_multilingual_options and compatible wpml_multilingual_options are stored/read with language suffixes, for example my_custom_option_en, my_custom_option_ru, or compatible codes like my_custom_option_uk
  • WP-LOC reads both the internal URL slug suffix and the compatible language-code suffix, so installations that use ua in URLs can still read legacy-compatible *_uk option values
  • Custom settings pages under Settings receive localized values in the admin when the admin language differs from the default language
  • Frontend AJAX requests to admin-ajax.php use the frontend language context, so transactional option reads inside AJAX handlers do not fall back to the default language
  • Code that explicitly calls do_action( 'wpml_switch_language', $lang ) or $sitepress->switch_lang( $lang ) temporarily switches WP-LOC's current language and WordPress locale until it is switched back. This is useful for cron jobs, payment webhooks, and transactional emails that need to render content in a user's saved preferred language

ACF content fields

  • The same multilingual ACF logic works for translatable posts/pages, classic editor post screens, Gutenberg page screens, and multilingual term edit screens
  • ACF picker fields for posts, pages, relationships, taxonomies, and nav menus only list choices from the current editor language
  • ACF user profile fields marked as translatable read/write language-suffixed user meta such as field_name_ru and field_name_en, while the default language keeps the canonical field_name meta key
  • shared fields resolve the source-language value but map media/post/term/menu references into the current language
  • shared fields stay editable on the source-language entity even when that source language is not the site default, and become readonly only on translated entities
  • Saving a translated post or term cannot push readonly shared values back into the translation group; shared sync runs from the source-language entity only
  • copy_once fields inherit the source-language value until the translated post/term/options page stores its own independent value
  • Container fields such as group, repeater, flexible_content, and clone preserve their ACF structure while mapping nested media and relation values per language

Yoast SEO

  • WP-LOC loads a dedicated Yoast compatibility layer only when Yoast SEO is active
  • The Yoast compatibility layer and Yoast sitemap alternate links can be toggled separately in Multilingual > Settings > Integrations
  • Global Yoast options such as wpseo_titles, wpseo_social, and wpseo_rss can be localized per language through the same multilingual options model used by WP-LOC
  • Yoast primary category meta is resolved to the translated term in the current post language
  • Yoast taxonomy SEO meta is copied into translated terms so translated archives keep their own SEO title/description state
  • A translated term that has no SEO meta of its own falls back to the source term's values. A translated term that does have its own keeps it: editing the SEO of any term in a translation group never overwrites its siblings, in either direction
  • Yoast indexables are invalidated after multilingual post, term, and global-option updates so Yoast can rebuild its cached SEO data
  • Yoast XML sitemaps gain xhtml:link alternate-language entries for translated posts, pages, terms, and first archive links
  • Yoast stripcategorybase rewrites remain compatible with multilingual category slugs
  • Yoast News can reuse the current post language code for publication-language output when the addon is active

Yoast Duplicate Post

  • Works with Yoast Duplicate Post's Clone, New Draft, and bulk Clone actions
  • When you clone a translatable post, WP-LOC also duplicates every sibling translation in its group and links all copies together as a fresh translation group, so the clone keeps its multilingual structure
  • Each sibling is duplicated through Duplicate Post's own engine, so copied meta, taxonomies, attachments, and comments follow your Duplicate Post settings
  • Rewrite & Republish is intentionally left untouched — it merges the copy back into the original, which keeps its existing translation links
  • Non-translatable post types are duplicated by Duplicate Post normally, with no translation-group handling
  • The behavior can be disabled per project with the wp_loc_duplicate_translation_group filter (return false)

In Twig (Timber)

{{ wp_loc_language_switcher() }}

{% for lang in wp_loc_languages() %}
  <a href="{{ lang.url }}" class="{{ lang.active ? 'active' : '' }}">{{ lang.name }}</a>
{% endfor %}

Taxonomy Notes

  • Enable multilingual behavior per taxonomy in Multilingual > Settings
  • Term translations are stored in the icl_translations table as tax_{taxonomy} rows using term_taxonomy_id
  • Category, tag, and custom taxonomy archive URLs are language-aware
  • Nested hierarchical term archive paths resolve segment-by-segment in the requested language, so duplicate child slugs across languages do not collapse to the default language
  • For translatable posts, multilingual taxonomy assignments sync across the whole post translation group
  • If a translated term does not exist for a target language, the frontend switcher hides that target or falls back to its language home URL according to the configured switcher behavior
  • Wrong-language term archive URLs return 404

Routing Notes

  • Translated singular URLs resolve by language, post type, and slug, so translated posts from different post types can safely share the same slug
  • Frontend slug resolution only considers publicly viewable post types, so a non-public object that happens to share a slug cannot surface as a public singular URL
  • Hierarchical page paths are resolved in the requested language before WordPress fallback resolution, so translated parent/child pages can safely reuse the same slugs across languages
  • Custom post type translations with identical slugs across languages resolve to their translated post instead of redirecting back to the default-language post
  • Canonical redirects are blocked when WordPress tries to strip or replace an existing non-default language URL prefix
  • The language prefix is applied to page_link, post_link, and post_type_link at priority 9999, after themes and plugins finish building custom (taxonomy-driven) permalink paths, so translated custom post type URLs keep their target-language path instead of losing the prefix
  • Compatibility switcher APIs such as icl_get_languages() use the same translated URLs as WP-LOC's native switcher helpers
  • Frontend requests persist wp_loc_current_language, wp_loc_current_locale, _icl_current_language, and wp-wpml_current_language cookies so same-origin AJAX calls to admin-ajax.php keep the expected language context
  • A cookie is only written when its value actually changes. Re-sending an identical cookie would be invisible to the visitor but adds a Set-Cookie header, and shared caches treat any response carrying one as personalised — so a CDN would refuse to store it. Since language is resolved from the URL prefix, a returning visitor's requests stay header-clean and remain cacheable at the edge
  • For AJAX, the referring URL is trusted ahead of those cookies. A page served from a full-page cache never runs PHP, so it never sends Set-Cookie, and the cookie can still name the language the visitor left one switch ago; the referer always names the page the request actually came from. A same-site referer with no language prefix resolves to the default language, since that is the language served without one. The cookies remain the fallback for requests that arrive with no usable referer, such as under Referrer-Policy: no-referrer
  • Language resolution order for AJAX: explicit lang request parameter → query var → URL prefix → referring URL → cookies → default language
  • Language resolution is safe to call from inside an option filter. Resolving the current language itself reads options, so a theme or plugin that registers a broad pre_option / option_* filter and asks WP-LOC for the current language from inside it — for example through $sitepress->get_current_language() — would otherwise send the two into a loop that ends only when PHP runs out of memory. Such a re-entrant call is answered immediately with the default language instead, and the outer call goes on to resolve the real one

Compatibility Note

WP-LOC can interoperate with sites that already use the icl_translations table and legacy multilingual config files such as wpml-config.xml.

WP-LOC also integrates with Yoast Duplicate Post when it is active, so cloning a post keeps its translation group intact. The integration only hooks Duplicate Post's own actions and does nothing when the plugin is not installed.

WP-LOC is an independent open-source project. It is not affiliated with, endorsed by, or sponsored by any third-party multilingual plugin vendor.

License

GPLv2 or later.

Author

Vitalii Kaplia — kaplia.pro