Lifetime prices rise on 19 October →
Phox Elements

Overview

Phox Translate turns a Breakdance, Oxygen, Bricks or vanilla WordPress site into a multilingual site. It translates your pages, headers, footers, templates, popups, products and SEO with DeepL or OpenAI, and creates real WordPress pages you can edit like any other.

The core idea: every translation is an ordinary WordPress post, linked to its source. There's no proxy, no cloud dashboard and no shortcode soup. Visitors on a translated page get a fully translated experience: the header, footer, templates and popups all swap to their translated variants automatically, post loops show only that language, and the site locale follows along so even WooCommerce checkout strings match.

Site-wide wizard

Audits everything, estimates the cost, then translates the whole site in the background.

33 languages

DeepL or OpenAI with your own API key. You pay provider rates directly, with no markup.

The whole shell

Headers, footers, templates, popups and global blocks all render in the visitor's language.

WooCommerce

Products, attributes and variable products, with one set of stock and prices shared across every language.

Stays in sync

Edit a source page and its translations are flagged Out of date, with one-click re-translation.

SEO built in

hreflang tags, translated slugs and translated SEO meta. Real indexed pages, no cloaking.

Breakdance, Oxygen, Bricks or vanilla WordPress

One plugin supports all four. With Breakdance or Oxygen 6+ you get the full integration: element trees, headers/footers/templates/popups, global blocks (Components in Oxygen) and the builder element. With Bricks you get element content, all template types (headers, footers, sections, popups) and a native Bricks element. The active builder, its post types and data formats are detected automatically. On a vanilla site you still get post content (block or classic), SEO meta and custom-field translation plus the shortcode switcher.

Requirements & installation

RequirementMinimum version
WordPress6.0 or newer
PHP7.4 or newer
Breakdance, Oxygen or BricksOptional, recommended. Tested with Breakdance 2.8, Oxygen 6.1 and Bricks 2.2
WooCommerceOptional. Tested against WooCommerce 10.6 and 11.1
A DeepL or OpenAI accountFor the API key
  1. Install the plugin. In WordPress go to Plugins → Add New → Upload Plugin, choose the phox-translate.zip file and click Install Now.
  2. Activate it. A new Phox Translate item appears in the main admin menu.
  3. Add your API key. Open Phox Translate → Provider, choose DeepL or OpenAI, paste your key and click Test Connection.
  4. Choose your languages. On the General tab set your default language and tick your target languages.
Permissions

Settings, globals and the wizard require the Administrator capability (manage_options). Translating an individual page from its edit screen requires edit rights on that specific post.

Updating from an earlier version

Updates are safe to apply directly; no content is ever rewritten in place. On the first admin page load after an update the plugin runs any outstanding migration steps, most of which arm a background worker rather than doing the work inside your request. Large sites should give it a few minutes of normal admin traffic to finish.

Quick start: your first translation

Let's translate one page into French, end to end.

  1. Configure once. On Phox Translate → General, set your default language (e.g. English) and tick French as a target. On the Provider tab add your API key and test the connection.
  2. Open any page in the WordPress editor. In the sidebar you'll find the Phox Translate box listing your target languages.
  3. Click Translate Page. A new French page is created as a draft, linked to the original, with the layout intact and every text element translated. The box shows an Edit link per language.
  4. Review and publish. Open the draft in your builder if you want to tweak anything, then publish it. Its URL will be /fr/page-name.
  5. Add the switcher. Drop the Language Switcher element into your header (search the element panel), or use the [phox_language_switcher] shortcode. Visitors can now hop between languages.

When you're happy with how one page behaves, use the site-wide wizard to do the whole site in one run.

The admin interface

Everything lives under the Phox Translate top-level menu, organised into seven tabs:

TabWhat you configure
GeneralDefault language, target languages, URL style, the language-root homepage, the untranslated-content fallback, which post types are translatable, and flag overrides for the switcher.
ProviderDeepL or OpenAI, your API key and a connection test.
GlobalsHeaders, footers, global blocks, templates and popups, with per-language status and one-click translate buttons.
WizardThe site-wide translation flow: audit, options, confirm, progress.
Strings NewWooCommerce product attribute labels per language, edited by hand or translated in one click. Hidden when WooCommerce isn't active.
AdvancedThe text-key whitelist, excluded terms, HTML preservation, auto-linking, the flag font, and the SEO / custom fields / WooCommerce toggles.
LicenseActivate your license for updates and the wizard.

In the post lists

On Pages, Posts and any enabled type you also get:

  • A Language column showing each post's language badge, and an Out of date badge with a Re-translate link when a source has changed since its translation was made.
  • A language filter dropdown and a translation status dropdown (Out of date / Up to date).
  • A Re-translate (overwrite) bulk action, which queues the selected posts through the same background worker the wizard uses.

Languages & URLs

Default and target languages

The default language is the language your content is written in. Target languages are the ones you translate into; 33 are supported, covering everything DeepL offers plus additional languages via OpenAI.

URL style

StyleExampleHow it works
Prefix (default)/fr/aboutHierarchical types (pages) are placed under a hidden per-language parent, so URLs read like folders. Non-hierarchical types (posts, most CPTs) get a language suffix on the slug.
Suffix/about-frThe language code is appended to every translated slug.

Slugs themselves are translated: an English page at /landscaping becomes /fr/amenagement-paysager, not /fr/landscaping. For languages that don't romanise cleanly (Japanese, Arabic, Russian and so on) the original slug is kept so URLs never come out empty.

Language hub pages

Prefix mode creates one hidden "hub" page per language (slug fr, de, etc.) to act as the URL parent. Hubs are noindexed, excluded from search and hidden from listings; they exist purely to build the URL structure.

Prefix mode and non-hierarchical types

Folder-style URLs are built from a real parent page, which only hierarchical post types can have. Posts and flat CPTs always use the suffix form (/blog/about-fr) whichever mode is selected. The General tab names the affected types under the setting so it never looks like the option was ignored.

Translated homepage at the language root

With this on (the default, prefix mode only) the translated homepage becomes /fr/ itself rather than sitting at /fr/home. Existing homepage translations are not moved when you change the setting — it applies to translations created afterwards.

Post types

Choose which public post types are translatable. Pages and posts are enabled by default; add WooCommerce products or any custom post type from the same list. Your builder's own global post types (templates, headers, footers, blocks) are deliberately not listed here — they're handled by the Globals tab, and translating them twice would create orphaned duplicates.

Flag overrides

For languages spoken in several countries, choose which flag the switcher shows: 🇺🇸 or 🇬🇧 for English, 🇧🇷 or 🇵🇹 for Portuguese, and so on.

Translation providers

You bring your own API key and pay the provider directly. Typical cost is pennies per page.

ProviderBest forNotes
DeepLEuropean languages, best-in-class qualityFree keys (ending :fx) and Pro keys are auto-detected and routed to the right endpoint. The free tier includes 500,000 characters per month.
OpenAIBroadest language supportUses GPT-4o-mini with a translation-tuned prompt. Slightly cheaper than DeepL Pro at typical volumes.

Enter the key on the Provider tab and click Test Connection; DeepL responses include your current usage against your quota.

Key storage

API keys are stored encrypted (AES-256 with a per-encryption random IV, keyed off your site's auth salt), never in plain text. The key is write-only in the UI; the field shows a mask once one is saved.

HTML preservation

Rich text often contains inline HTML (links, bold, line breaks). With Preserve HTML on (the default), DeepL is instructed to translate only text nodes, and OpenAI is prompted to leave tags untouched, so your markup survives translation intact.

See Reliability & long content for how requests are chunked, timed out and retried.

License

Enter your license key on the License tab and click Activate. An active license enables plugin updates and unlocks the site-wide wizard. Translating individual pages and globals works without a license, so you can evaluate the plugin fully before committing to a bulk run.

Single pages & posts

Every translatable post gets a Phox Translate meta box in its editor sidebar showing:

  • The post's language, and its source if it is itself a translation.
  • Existing translations with their publish status and edit links.
  • Checkboxes for each missing language and a Translate Page button.

Translations are created as drafts so you can review before anything goes live. The whole layout is carried over; only the text changes. Internal links in the content are rewired to the translated versions of the pages they point at, wherever those exist.

Updating a translation

Once a translation exists, translating again skips it by default (your manual edits are safe). To re-translate after changing the source, use the Re-translate link on the post list, the Re-translate (overwrite) bulk action, the wizard with Overwrite existing translations ticked, or the Re-translate button on globals. Overwriting replaces the translated content, title, slug and excerpt with a fresh translation of the current source.

The plugin tells you when that's worth doing — see Keeping translations current.

Headers, footers, templates & popups

The Globals tab lists every builder global: headers, footers, global blocks (Components in Oxygen, Templates in Bricks), templates and popups. Each row shows a status dot per target language (with draft states flagged), plus Translate and Re-translate buttons.

Translated globals are always published (they're never viewed standalone, and the builders' own managers only list published items). On the front end the correct variant is swapped in at render time based on the page's language, so:

  • A French page renders the French header, footer and any matching templates (blog layouts, archives, search results, 404).
  • Popups render in French too, whether they fire automatically or from a button, link, form action or interaction.
  • Global blocks used as post loop card templates swap as well, so loops on translated pages use the translated card design.
Order matters less than you'd think

If you translate a header before the pages it links to exist, don't worry: after every wizard batch the plugin re-runs its link and reference fix-ups across all translations, so menus, buttons and popup references end up pointing at the right language.

The site-wide wizard

The wizard translates your entire site in one guided flow. It requires an active license, a configured provider key and at least one target language; it will tell you (with links) if anything's missing.

  1. Audit. The wizard scans every enabled post type plus all globals, taxonomy terms and menu labels, and reports totals, untranslated counts and character counts per type.
  2. Select. Untick any content type, or expand a type to exclude individual pages. Taxonomy terms and menu labels have their own include toggles.
  3. Options. Choose the target languages for this run, whether translations are created as drafts or published immediately, whether existing translations are overwritten or skipped, and an optional delay between API requests.
  4. Confirm. A summary shows the exact number of jobs, estimated characters and an estimated cost for your chosen provider, before a single API call is made.
  5. Run. A progress bar tracks every job. You can watch it, or click Run in Background and close the tab; processing continues server-side via WP-Cron, and reopening the Wizard tab resumes the progress view automatically.
  6. Finish. The completion screen lists anything that failed (with the reason and a one-click Retry Failed Items), and walks you through next steps: review drafts, add the language switcher, check the Globals tab.

How the batch behaves

  • Jobs live in a dedicated pt_batch_queue database table, one row per job, inserted once and deleted once — so a large run doesn't rewrite a giant option on every job.
  • Without Overwrite, only missing languages are queued per item, so the job count always matches the estimate and nothing you've edited is touched.
  • A database lock guarantees each job runs exactly once, even with the tab open and cron running together.
  • A job's attempt counter is incremented before the translation runs and the row deleted only after it succeeds, so a request killed by a memory or time limit is retried rather than silently lost.
  • One failing page never stops the batch; it's logged and reported on the completion screen with a retry button.
  • After the last job, the plugin re-runs URL and reference fix-ups across all translations so cross-links, global block references and popup references all point at the right language.

Taxonomy terms

Categories, tags and WooCommerce attribute terms are translated with a display-only overlay. Term names and descriptions are translated once (as part of a wizard run) and stored as term meta; on the front end, pages in another language display the translated name while the term itself keeps its ID, slug and taxonomy.

This design is deliberate: because nothing is renamed in the database, queries can't break. Builder query controls, tax queries, Phox Facets filters and archive URLs all keep matching against the real values, while visitors see translated labels on cards, archives, facets and product pages.

Translated posts also carry the same term assignments as their sources, so category archives and filtered loops return the right posts in every language.

Editing term translations

Every translated name is editable by hand. Open the term's edit screen (e.g. Posts → Categories → Edit) and you'll find a Phox Translate section with a name field per target language (and a description field where relevant). Empty a field to fall back to the original name. Note that re-running the wizard's term pass with Overwrite replaces manual edits with fresh machine translations.

Scope

The overlay applies on the front end only. wp-admin, the block editor and REST responses always see the original names, so editing is never confusing.

Functional taxonomies are never translated

product_type, product_visibility and post_format hold machine values, not labels — WooCommerce reads product_type to decide whether a product is simple or variable. These are excluded from translation entirely, and any translations a previous version created for them are removed automatically on update.

WordPress navigation menus (built under Appearance → Menus) sit outside the builder tree, so they're translated with the same display overlay approach as taxonomy terms — no menu is ever duplicated. On a translated page, each menu item is rewritten live:

  • Links point at the translated version of the page they target (a "Details" item leads to /fr/details), and custom links are matched against the internal URL map.
  • Labels are translated. Menu labels are translated once as a wizard step and stored as menu-item meta; where an item just uses a page's title, the translated page title is used automatically.

Include Menu labels in a wizard run to translate them, or edit any translation by hand: each menu item on Appearance → Menus gains a Phox Translate labels field per target language. Leave a field blank to fall back to the automatic behaviour.

The URL map behind this is cached per language and invalidated whenever a translation is linked, unlinked, published or deleted, so menus never lag behind your content.

Builder menus vs WP menus

Menus built directly in a Breakdance/Oxygen/Bricks header element are already covered by the element-tree translation and template swap. This section is specifically for classic wp_nav_menu menus that a header pulls in by location.

WooCommerce & variable products

Enable Product on the General tab's post-type list and products translate like any other post: title, description, short description, slug, attributes and purchase notes. Attribute values are terms, handled by the term overlay; attribute labels have their own screen, see Attribute labels.

Variable products are display shells New

A translated variable product carries the translated title, description and attributes, and borrows the source product's real variations. No product_variation posts are ever created for a translation.

That means one set of commerce data for the whole shop. Stock, price, SKU and sale status live on the source product in every language. A sale in French decrements the same stock number as a sale in English, and a price change is made once.

What the visitor seesWhere it comes from
Title, description, attribute labels and valuesThe translated product
Variation dropdowns / swatches, images, prices, stockThe source product's variations
Cart and checkout line names, and the "added to cart" noticeThe translated product — including the block cart and checkout
The line item's link back to the productThe translated product's URL
Stock decrement, order totals, emails, reportsThe source product

Every order line records the language it was bought in as _pt_lang line-item meta, so you can tell which language store a sale came from.

When borrowing applies

Two rules keep this from ever changing what an existing shop does:

  • Variable only. Both the translation and its source must be variable products. Simple products are never touched.
  • Nothing of its own. A translation that already has its own variation posts (from an older version, or created by hand) keeps them and borrows nothing.
Existing broken shells heal themselves

Translated variable products created before 3.9.0 had no variations at all and were not purchasable. On update they're repaired in the background — they become variable, borrow the source's variations and start selling again, with no re-translation and no API spend.

The product lookup table

WooCommerce keeps a denormalised wc_product_meta_lookup row per product and joins it for sorting, filtering and SKU resolution. Because translations are created with ordinary WordPress functions, that row has to be written explicitly — otherwise a translated product sorts as though it were free on Sort by price, and price-filter widgets and SKU lookups miss it. Phox Translate refreshes the row whenever it writes a product, and a background pass repairs any that pre-date the fix.

Locale and checkout strings

Translated pages switch the WordPress locale, so WooCommerce's own strings (checkout labels, validation messages, emails) render in the page's language, provided the relevant language pack is installed.

Known limitation: cart and checkout page identity

A translated cart or checkout page is a different WordPress page from the original, and WooCommerce's own is_cart() / is_checkout() checks don't yet know about the link. Phox Translate resolves the translated pages itself where it needs to (line-item naming, for example), but third-party code that calls those functions directly may not recognise a translated cart page. Building the cart and checkout with blocks, or with a template that isn't conditional on those functions, avoids it.

Attribute labels & strings New

A WooCommerce global attribute has a label — "Colour", "Size", "Amsterdam Acryl Color" — that lives once for the whole shop, not per product and not as a term. It's the heading above the dropdown on the product page, and the prefix on cart and order lines.

The Strings tab lists every global attribute with a field per target language:

  1. Translate empty labels. One click sends every label that has no translation yet to your provider and fills the fields in. This also happens automatically the first time you translate a product into a language you haven't used before.
  2. Edit by hand. Change any field and save. Leave a field empty to show the original label in that language.
  3. Save. Changes apply immediately on the front end — products do not need re-translating.

As with terms, the attribute is never renamed, re-slugged or duplicated. The translation is an overlay applied at render time, so every tax_query, facet and variation match keeps working against the real database values.

Custom attributes

Attributes created on a single product (rather than under Products → Attributes) aren't global, so they don't appear here. Their names and values are translated with the product itself.

Keeping translations current New

Edit an English page and its French translation is now wrong. Phox Translate notices and says so.

  • Translations whose source has changed get an Out of date badge in the Language column, with a Re-translate link beside it.
  • The post list gains an Any translation status filter with Out of date and Up to date, so you can see the whole backlog at once.
  • Select several and use the Re-translate (overwrite) bulk action to queue them through the background worker.

What counts as a change

The comparison is made on the text that would actually be sent to the translation provider, not on the raw post record. Restyling a section, dragging elements around or re-saving in the builder changes the stored data but not the strings, so it doesn't flag anything. Editing a heading does.

Changes are detected three ways, because builders don't all save the same way: the normal editor save, direct writes to builder content meta (Breakdance and Bricks bypass wp_update_post entirely), and an hourly background scan that catches WP-CLI imports, direct SQL and anything else that skips WordPress hooks.

Existing translations start clean

The plugin has no record of what a source said before this feature existed, so on update every current translation is recorded as up to date. Only changes made from then on flag anything — an upgrade never turns your whole site orange.

What gets translated

For each post, the plugin translates:

  • The builder tree: every text-bearing property across the element library, including headings, rich text, buttons, form labels, placeholders, submit and success/error messages, login form strings, FAQ questions and answers, slider labels, counters, image alt text and ARIA labels.
  • Post basics: title, slug (translated naturally, then sanitised), excerpt and post content.
  • Block and classic content: block content is parsed and only the text inside each block is sent, so block delimiters, attributes and class names come back byte-identical.
  • SEO meta: Yoast and RankMath titles and descriptions. Source-language SEO values are never copied verbatim onto translations.
  • Custom fields: ACF and Phox Studio text, textarea and WYSIWYG fields, including repeater and group sub-fields.
  • WooCommerce products: custom attribute names and values, and purchase notes. Taxonomy-based attribute values are handled by the term overlay and their labels by the Strings tab.

Alongside the text, the plugin also rewires references inside the copied layout: internal links point at translated pages, global block IDs point at translated global blocks, and popup references point at translated popups.

Custom elements

Text is extracted by property key against a whitelist that ships tuned for the whole Breakdance 2.8 element library. If a third-party element stores text under an unusual key, add that key on the Advanced tab and re-translate.

The language switcher

The switcher lets visitors change language. One renderer powers three ways of placing it, with identical markup and styling:

MethodWhere
Builder elementSearch "Language Switcher" in the Breakdance/Oxygen element panel or the Bricks elements list. Typically placed in the header.
Shortcode[phox_language_switcher] anywhere shortcodes work: Gutenberg, widgets, classic editor.
Template tagpt_language_switcher() in theme files.

Options

OptionValuesShortcode attribute
Display styleDropdown, inline links, flags only, flags + textstyle="dropdown"
Flag styleRectangle, circle or noneflags="rectangle"
Show current languageOn / offshow_current="1"
Untranslated pagesLink to that language's homepage, or hide the languagemissing="home"

Smart fallbacks

If the current page has no translation in a given language, the switcher doesn't dead-end. By default it links that language to its translated homepage, so the visitor still lands in a fully translated shell. Fallback links carry a --fallback CSS class if you want to style them differently. A language with no page translation and no translated homepage is simply omitted.

// Shortcode examples
[phox_language_switcher]
[phox_language_switcher style="flags" flags="circle"]
[phox_language_switcher style="inline" show_current="0" missing="hide"]
Flags showing as "GB", "FR"?

Every current browser draws flag emoji natively, so the plugin ships none by default. A few older platforms don't; if yours is one, tick Load the bundled flag font on the Advanced tab. It's off by default because it adds a 78 KB font download that almost no site needs.

How pages are served

When a visitor lands on a translated page, several things happen automatically:

  • Globals swap. The header, footer, matching templates and popups are exchanged for their translated variants before rendering.
  • Queries are scoped. Archives, search results and post loops for translatable post types only return posts in the current language. On default-language pages, translated variants are hidden. Loops can opt out with the pt_skip_lang_filter query var.
  • The locale switches. The plugin calls switch_to_locale() to match the page language, so WordPress core, WooCommerce and other plugins render their own strings (checkout, dates, validation messages) in the right language, provided the relevant language packs are installed.
  • Terms overlay. Category, tag and attribute names display translated.
  • Search stays in language. Search forms on translated pages carry a language marker, so results and the search results template stay in the visitor's language, including Phox Elements search bars with facet parameters.
  • Facets stay in language. Phox Facets refreshes are language-scoped, so filtered loops, counts and ranges match the page's language.

Language scoping is driven by a hidden pt_language taxonomy that indexes what the post meta already says. It's an index, never the source of truth: until the index has finished building on a site that's just updated, the plugin falls back to querying the meta directly, so a half-built index can never hide a post.

Untranslated content New

By default, content you haven't translated yet simply doesn't appear in other languages. On a shop with 500 products and 80 translated, the French catalogue shows 80 products.

Tick Show untranslated content in the original language on the General tab and the other 420 appear too, in the original language, and stay purchasable. Content you have translated is unaffected: the translated version shows and the source stays hidden, exactly as before — you never see both.

Clicking a fallback item takes the visitor to its original-language URL, because there is no translated URL to go to.

It arms itself in the background

The setting needs a per-language index over your existing translations, built by a background worker after you switch it on. Until that finishes, the setting shows "Preparing" and behaves as if it were off — a half-built index would list a source alongside its own translation. On most sites it takes a minute or two.

Performance note

The fallback query needs an OR relation, which disables WordPress's query cache on 6.1+. It's a real cost, paid only while the option is on. Leave it off if your site is fully translated.

SEO

  • hreflang. Every published page in a language group outputs <link rel="alternate" hreflang="…"> tags for its siblings, plus x-default pointing at the source language.
  • Translated SEO meta. Yoast and RankMath titles and descriptions are translated; canonical URLs and source-language values are never duplicated onto translations, avoiding duplicate-content signals.
  • Clean URLs. Prefix (/fr/page-name) or suffix (/page-name-fr) structures, with translated slugs.
  • Hub pages are noindexed and kept out of search and listings.
  • Correct lang attribute. The <html> tag carries the page's own locale.
  • Real pages. Because translations are ordinary posts, they're crawled and indexed like any other page. No JavaScript translation, no cloaking.

Advanced settings

SettingPurposeDefault
Text keys to extractThe whitelist of builder JSON property keys treated as translatable text. Tuned for the full Breakdance 2.8 element library out of the box; extend it for third-party elements.~50 keys
Excluded termsBrand names, product names or phrases that must never be translated. One per line, matched as whole words and case-insensitively; the original spelling is kept in every language.Empty
Preserve HTMLKeep inline markup intact during translation.On
Auto-link new pagesCreate the language group link automatically when a new page is translated.On
Translate SEO metaYoast / RankMath titles and descriptions.On
Translate custom fieldsACF and Phox Studio fields, including repeaters and groups.On
Translate WooCommerceProduct attributes and purchase notes.On
Load the bundled flag fontOnly for platforms with no flag-emoji glyphs. Adds a 78 KB download.Off

Excluded terms apply to future translations; content already translated is unaffected until you re-translate it.

Excluded terms are enforced differently by each provider

DeepL is told mechanically, by wrapping each occurrence so the API cannot translate it — a hard guarantee. OpenAI is asked, in the prompt. That is usually enough, but a model can still accent or localise a brand name, or occasionally over-apply the instruction and leave a nearby phrase untranslated. If exact brand fidelity matters, DeepL is the safer choice.

Careful with keys

Only add keys that hold visible text. Keys like value, url or name often hold identifiers; translating them can break form submissions or queries. When unsure, use the diagnose endpoint to see exactly what the parser extracts from a page.

Reliability & long content

Translation requests go straight from your server to your provider — there is no Phox-owned API in the path. Three mechanisms keep long or unlucky requests from failing:

Size-aware chunking

Strings are batched by character count as well as string count, and a single string longer than the limit is split at points where the HTML tag depth returns to zero, so every part is balanced markup and the parts rejoin byte-for-byte. This matters for long-form content: a 18,000-character article written as one HTML block used to go out as a single request and time out.

Timeouts and retries

PhaseBehaviour
DNS / connect30-second connect timeout, raised from WordPress's fixed 10 seconds specifically for your provider's host, and retried up to 3 times with backoff. A failed name lookup is transient and cheap to retry.
Transfer180-second request timeout, retried once. A long request has probably already cost provider time, so it isn't hammered.
Unreadable responseIf the model returns something that isn't valid JSON, the batch is re-sent as two halves rather than re-sending the identical payload. Errors report the finish reason and the start of the response, so a truncated reply is distinguishable from a malformed one.

Error messages record how many attempts were made, so "cURL error 28 (after 3 attempts)" tells you the network genuinely wasn't reachable rather than that one packet went missing.

If your host reports "no outbound requests"

They're looking at inbound web-request logs. A translation is a server-side PHP call to api.openai.com or api.deepl.com; it never appears there. Provider rate limiting returns an HTTP 429 immediately — it doesn't present as a long silence.

Repairs, migrations & self-healing

Updates

The plugin keeps a database version and runs any outstanding steps once, in order, on the first admin load after an update. Each step is idempotent and guarded: a marker is written before the step runs and cleared afterwards, so a step killed by a time limit is detected and retried. After two failed attempts the runner stops and shows an admin notice with a Retry now link, rather than looping on every page load or leaving the site half-migrated.

Steps that have to touch every post — building the language index, hashing sources for staleness tracking, repairing WooCommerce lookup rows, building the untranslated-content markers — don't run inside your admin request. They arm a cursor and a background worker that processes a few hundred rows per tick, so the size of your catalogue never decides whether an update succeeds.

The Globals tab as a repair pass

Opening the Globals tab also runs a set of idempotent fix-ups (they write only when something is actually wrong):

  • Publishes any global translation stuck as a draft.
  • Repairs builder meta on translated posts (template settings, conditions) if a copy was ever corrupted.
  • Re-copies taxonomy term assignments from sources to translations.
  • Re-runs the global block / popup ID swap and the internal URL swap across every translation.
Rule of thumb

If anything ever looks out of sync after an update (a missing popup, an English link on a translated page), open the Globals tab once and re-check. It's the first thing support will ask you to do.


Data model

Translations are ordinary posts of the same post type as their source, linked by post meta. Nothing is stored outside your database, and deactivating the plugin leaves every translated page in place.

Post meta

WhereKeyHolds
Any post_pt_languageThe post's language code (set on sources once they have a translation).
Source post_pt_translationsMap of lang => post ID for its translations.
Translated post_pt_sourceThe source post's ID.
Translated global_pt_is_global_translationMarks headers/footers/templates/popups/blocks so they're excluded from the builder's condition matching.
Hub page_pt_language_hubMarks the hidden per-language URL parent.
Translated post_pt_source_hash · _pt_staleStaleness baseline and the denormalised comparison result (so the admin filter is indexable).
Source post_pt_source_hash_cacheFingerprint + hash pair, so recomputing staleness in a list table stays cheap.
Order line item_pt_langThe language the shopper bought in.
Term_pt_name_{lang} · _pt_desc_{lang}Translated term name / description for the display overlay.
Menu item_pt_menu_label_{lang}Translated nav-menu label.

Taxonomy, options and tables

KindNameHolds
Taxonomypt_languageHidden index for language-scoped queries. Translations carry their language code, hubs carry pt-hub, and sources carry a pt-has-{lang} marker per language they've been translated into. Sources and untranslated posts otherwise carry no term. The meta above stays authoritative.
Optionphox_translate_settingsAll plugin settings (the API key inside is encrypted).
Optionpt_attribute_labels[ lang ][ taxonomy ] => label for the WooCommerce attribute-label overlay.
Optionphox_translate_db_version · phox_translate_migration_stateThe migrations runner's version number and crash sentinel.
Optionpt_lang_tax_ready · pt_markers_readyGates: the language index and the untranslated-content markers are only trusted once their background backfill has completed.
Table{prefix}pt_batch_queueOne row per queued translation job.
Builder data

The translated layout is written back to the builder's own meta key (_breakdance_data, _oxygen_data, Bricks' content keys) in that builder's format, and the plugin invalidates the builder's CSS and dependency caches whenever it writes, so styles regenerate from the translated content.

REST API

Endpoints live under the phox-translate/v1 namespace.

MethodRouteAccessPurpose
POST/translateedit_post on the targetTranslate one post into one or more languages: { post_id, languages: [] }.
POST/wizard-auditAdmin + licenseRun the site audit and return the manifest.
POST/wizard-startAdmin + licenseQueue a batch. Items are derived server-side; the client sends only languages, options and exclusion filters.
GET/wizard-progressAdmin + licenseReport batch progress; each poll also processes one queued job.
GET/batch-errorsAdminThe recent batch error log.
GET/diagnose/{post_id}AdminDump a post's builder data structure and exactly what the parser extracts. Invaluable when tuning text keys.
GET/translation-urls/{post_id}PublicPublished translation URLs for a post (powers external switcher integrations).

Product diagnostics

An admin-only, nonce-checked AJAX action pt_diagnose_product reports everything that decides how a translated product behaves: its post type and status, its source link and language, its product_type terms as stored and as filtered, the class WooCommerce's factory builds for it, how many children it resolves, and whether it is borrowing its source's variations. Use it when a translated product renders as simple.

Hooks, query vars & template tags

Template tags & helpers

// Echo the language switcher anywhere in a theme.
pt_language_switcher( [ 'display_style' => 'inline' ] );

// Current page language ('fr') and the site default ('en').
$lang    = pt_get_current_page_language();
$default = pt_get_default_language();

// Published translation URLs for a post: [ 'en' => url, 'fr' => url ].
$urls = pt_get_translation_urls( $post_id );

// Every supported language code => name, and the configured excluded terms.
$langs    = pt_get_supported_languages();
$excluded = pt_get_excluded_terms();

// Is the untranslated-content fallback active AND finished building?
if ( pt_fallback_untranslated_enabled() ) { /* … */ }

Filters

// Change the locale used on translated pages (default maps 'fr' to 'fr_FR' etc).
add_filter( 'pt_switch_locale', function ( $locale, $lang ) {
    return $lang === 'fr' ? 'fr_CA' : $locale;
}, 10, 2 );

// Opt a site out of translating variable products entirely.
add_filter( 'pt_skip_variable_products', '__return_true' );

// Skip the wc_product_meta_lookup refresh for a product (e.g. a plugin owns it).
add_filter( 'pt_refresh_woo_lookup', function ( $refresh, $product_id ) {
    return $refresh;
}, 10, 2 );

Query vars

VarEffect
pt_filter_langSet on a custom WP_Query to opt in to language filtering.
pt_skip_lang_filterSet on a query to opt out of language filtering entirely.
pt_lang (URL param)Marks the request language on URLs with no queried object, such as search results. Added to search forms automatically.

Troubleshooting

The wizard button is greyed out

The wizard needs an active license, a working provider key and at least one target language. Warnings above the button link to whichever tab needs attention.

A translated global isn't showing in the builder's own manager, or a translated page shows an English popup / link / card

Open the Globals tab once. It runs the full repair pass, which fixes stray drafts, re-points references and re-swaps URLs across all translations. Then hard-refresh the front end.

A translated variable product shows no swatches, no price and no add-to-cart

It has been built as a simple product, which happens to translations created before 3.9.0. Updating fixes it: a background pass repairs every affected product without re-translating anything. If one is still wrong afterwards, run the product diagnostics action against it — wc_type tells you what WooCommerce thinks it is, and borrows whether it is picking up its source's variations.

A translated product sorts as though it were free, or a SKU lookup misses it

Its wc_product_meta_lookup row is missing. This is repaired in the background after an update; you can force it by re-saving the product.

A loop or archive shows posts from both languages

Language scoping applies to queries for the post types enabled on the General tab. Check the post type is ticked there, and that the loop's query targets that type explicitly. A custom query can opt in with pt_filter_lang.

Untranslated products vanished when I switched language

That's the default. Turn on Show untranslated content in the original language on the General tab, and give the background index a minute to build.

Checkout or date strings are still in the default language

Locale switching relies on WordPress language packs. In Settings → General, temporarily switch the site language to the target language once; WordPress downloads the core (and WooCommerce) translation files, after which you can switch back.

Some element text wasn't translated

The element probably stores that text under a property key not in the whitelist, common with third-party elements. Call the diagnose endpoint for the page, find the key holding your text, add it to Text keys on the Advanced tab, and re-translate with Overwrite.

"cURL error 28: Operation timed out" or "Resolving timed out"

"Resolving timed out" is DNS — your server couldn't look up the provider's hostname. The plugin already raises the connect timeout and retries three times, so a message that says (after 3 attempts) means the name genuinely wasn't resolvable; ask your host about the server's DNS resolver. "Operation timed out after 180000 ms" is a genuinely enormous single request; check whether the page holds one very large HTML block.

"Failed to parse OpenAI response as JSON"

The model returned something unreadable. The plugin automatically retries by splitting the batch in half, so a one-off is invisible; a persistent one is reported with the finish reason and the beginning of the response, which distinguishes a truncated reply from a malformed one.

The batch seems stuck

Background progress relies on WP-Cron, which advances when the site gets traffic. Reopen the Wizard tab: every progress poll also processes a job and re-arms the background worker. Persistent failures appear on the completion screen with reasons and a retry button; the most common cause is an exhausted provider quota.

An admin notice says a migration step failed

The runner stops after two attempts rather than retrying forever. The notice names the step and carries a Retry now link. If it fails again, that step's details are worth sending to support — nothing else is blocked in the meantime.

The switcher renders nothing

The switcher needs at least two languages with somewhere to link. Check target languages are configured and the homepage is translated (it's the fallback target for untranslated pages).

FAQ

Do I need Breakdance?

No, but it's where the plugin shines. On a vanilla site you still get post/SEO/custom-field translation, language-scoped queries, hreflang and the shortcode switcher. With Breakdance, Oxygen or Bricks you add full element-tree translation and automatic header/footer/template/popup swapping.

Can I edit a translation by hand?

Yes, that's the point. Translations are ordinary posts; open them in your builder or the editor and change anything. Manual edits are preserved unless you deliberately re-translate with Overwrite.

How do I know when a translation has gone out of date?

The plugin tells you. Edit a source and its translations pick up an Out of date badge in the post list, with a Re-translate link. See Keeping translations current.

Do I have to keep stock in sync across languages?

No. Translated variable products share their source's variations, so there is exactly one stock number, one price and one SKU per variation for the whole shop, whatever language the sale came in through.

Will my customers see the wrong language in the cart?

No. Cart lines, checkout lines, order emails and the "added to cart" notice all use the translated product name, in the classic cart and the block cart alike, and the line links back to the translated product page.

What does it cost to translate my site?

Whatever your provider charges, nothing more. The wizard shows a character count and cost estimate before starting. DeepL's free tier (500,000 characters per month) covers a typical small site in full.

What happens if I deactivate the plugin?

Every translated page remains published at its URL. You'd lose the automatic global swapping, query scoping, hreflang and switcher until you reactivate, but no content is ever removed.

Will translated categories break my product filters?

No. Term translation is a display overlay; IDs, slugs and taxonomies are untouched, so queries, facets and archives keep working exactly as before.

Can search work per language?

Yes. Searches made from a translated page carry a language marker, return results only in that language, and render through the translated search results template.

Is my API key safe?

It's stored encrypted with a random IV per encryption, never displayed back in full, and only ever sent to your chosen provider over HTTPS.

Changelog

3.9.4

Fixed

  • Translated variable products built as simple products on WooCommerce 11. WooCommerce 11 caches a product's type in the object cache. A translation whose cache entry was populated in the moment between the post being created and its product_type term being copied stayed a simple product permanently — no swatches, no price, no add to cart. The cache is now invalidated at every point the plugin writes a product's type, and a background pass repairs affected products with no re-translation and no API spend. Sites on WooCommerce 10 and earlier were never affected.

3.9.3

New

  • Product diagnostics. An admin-only pt_diagnose_product action reporting everything that decides how a translated product behaves — post type and status, source link, product_type terms stored and filtered, the class WooCommerce's factory builds, resolved children, and whether the product is borrowing its source's variations.

3.9.2

Fixed

  • Functional taxonomies are never translated. product_type, product_visibility and post_format hold machine values that WooCommerce and WordPress read to decide behaviour, not labels to show. The term overlay now excludes them, and any translations a previous version stored for them are purged on update.

3.9.1

Fixed

  • "Resolving timed out after 10000 milliseconds". WordPress fixes the DNS/connect timeout at 10 seconds and provides no way to change it through its own HTTP arguments. The plugin now raises it to 30 seconds for your provider's host specifically, and retries a failed connection up to three times with backoff.
  • "Failed to parse OpenAI response as JSON". The old retry re-sent the identical payload, which could not produce a different answer. An unreadable batch is now re-sent as two halves, and a persistent failure reports the model's finish reason, the response length and its opening characters, so a truncated reply is distinguishable from a malformed one.
  • Error messages now record how many attempts were made.

3.9.0

New

  • Variable product support. A translated variable product is a display shell: it carries the translated title, description and attributes and borrows the source product's real variations, so the shop keeps one set of stock, prices and SKUs across every language. Variation forms, swatches, cart and checkout lines, order emails and the block cart all show the translated name and link to the translated page, while stock and totals happen on the source. Orders record the language the shopper bought in. Existing translated variable products, which had no variations at all and could not be bought, are repaired automatically on update.
  • The Strings tab. WooCommerce global attribute labels — the heading above each dropdown, and the prefix on cart and order lines — are now translated per language, editable by hand and applied instantly without re-translating any product. Previously a fully translated product page still read "Amsterdam Acryl Color: Rojo".

3.8.3

New

  • Show untranslated content in the original language. A new General-tab option: instead of untranslated items disappearing when a visitor switches language, they appear in the original language and stay purchasable. Content that is translated behaves exactly as before. Off by default so an update never changes what visitors see.

3.8.2

Fixed

  • Timeouts on long-form content. Requests were batched by string count only, so an article written as one large HTML block went to the provider in a single call and timed out with nothing received. Batches are now capped by characters as well as count, an oversized string is split only where the markup is balanced, and the request timeout is 180 seconds. Reassembly is byte-exact.
  • Migrations could give up permanently on large sites. The language-index and queue-port steps were unbounded single-request loops; on a big catalogue they could never finish inside the PHP time limit, and the runner would exhaust its two attempts and stop. Both now process in chunks through a background worker.
  • "No update available" on a valid licence. If WordPress rebuilt its update transient outside an admin request (WP-CLI, cron), the plugin was left out of it and stayed invisible for up to 12 hours. The plugin is now re-inserted from cache on the read path.

3.8.1

Fixed

  • False "Out of date" flags caused by one-way translation links.

3.8.0

New

  • Stale-translation tracking. Edit a source and its translations are flagged Out of date in the post list, with a Re-translate link, a translation-status filter and a Re-translate (overwrite) bulk action. Detection compares the text that would actually be sent to the provider, so restyling, dragging elements or re-saving in the builder flags nothing. Builder saves that bypass WordPress hooks are caught by watching content meta directly, and an hourly scan catches imports and direct SQL.

3.7.0

Fixed

  • WooCommerce product lookup rows. Translated products had no row in wc_product_meta_lookup, which sorted them above every genuinely cheap product on "Sort by price" and broke SKU lookups, price-filter widgets and variable-product availability. Rows are now written whenever a product is translated, and a background pass repairs existing catalogues. Verified against a live 3,965-row store: no churn, no price changes beyond the one row that was actually wrong.

3.6.1

Fixed

  • The diagnose endpoint reported one string instead of the whole page on both Breakdance and Oxygen — the tool for finding a missing text key had never worked on the builders it exists for. It now reports the full list, plus a shortlist of text that isn't being extracted.
  • Enum values (select options that are identifiers, not labels) are no longer translated.

Changed

  • The bundled flag font is now opt-in. Every current browser draws flag emoji natively; the font added a 78 KB download for almost no one, and no reliable automatic detection exists.

3.6.0

New

  • A versioned migrations runner. One ordered pipeline with a crash sentinel replaces the accumulated one-time repair flags: a step killed mid-run is detected and retried, and after two failures the runner stops with a retryable admin notice instead of looping on every page load or leaving the site half-migrated.
  • A dedicated batch queue table. Wizard jobs are rows inserted once and deleted once, replacing an option that was rewritten twice per job.
  • A hidden language taxonomy indexing what the post meta already says, so language-scoped queries stop relying on meta queries. It's an index, never the source of truth, and the plugin falls back to meta until the index has finished building.
  • Block-aware content translation. Block content is parsed and only the text inside each block is sent, so delimiters, attributes and class names come back byte-identical.
  • A cached menu URL map, invalidated whenever translations are linked, published or deleted.

3.3.0

New

  • WordPress nav-menu translation. Classic wp_nav_menu menus now translate too, via a render-time overlay (no menu duplication): item links point at the translated pages and labels are translated. Add Menu labels to a wizard run, and fine-tune any label per language directly on the Appearance → Menus screen.

3.2.1

Fixed

  • Blank translations on large pages during a site-wide batch. On big builder trees the batch worker could hit PHP's memory or time limit mid-save, silently leaving a translation blank (with no error logged). Batch jobs now raise the memory limit, are retried automatically instead of being lost when a request is killed, and a job that repeatedly fails is reported as a visible, retryable error rather than a silent blank. A single page/template translated on its own was never affected.
  • Duplicate template translations. If a builder's own template CPT was added to the translatable post types, templates were translated twice (once as a page, once as a global), creating orphaned duplicates. The builder's global CPTs are now always handled by the Globals scan only.

3.2.0

New

  • Bricks support. Full integration with the Bricks theme builder: element content translation (with a Bricks-tuned key whitelist), all template types (headers, footers, sections, popups) translated and swapped per language via Bricks' own active-templates filter, Template element references repointed to translated variants, and a native Bricks Language Switcher element. The active builder is detected automatically; one plugin now covers Breakdance, Oxygen, Bricks and vanilla WordPress.

3.1.0

New

  • Oxygen 6 support. The plugin now works identically on Oxygen 6+ (which shares Breakdance's engine). Headers, footers, templates and Components are detected automatically, and the Language Switcher element appears in the Oxygen builder. No settings needed; the active builder is detected at runtime.

3.0.0

New

  • Taxonomy term translation. Categories, tags and attribute names display translated via a safe overlay that never touches slugs or queries, with per-language fields on the term edit screens for manual polish.
  • Language switcher everywhere. New [phox_language_switcher] shortcode and pt_language_switcher() template tag alongside the Breakdance element, all sharing one renderer.
  • Smart switcher fallbacks. Untranslated pages link to that language's homepage instead of showing "no translations", with a per-element choice to hide the language instead.
  • Popup support. Breakdance popups are audited, translated and swapped per language, including popups opened from links, forms and interactions.
  • Locale switching. Translated pages switch the WordPress locale so WooCommerce and core strings follow the page language.
  • Language-aware search. Searches from translated pages return results in that language through the translated search results template.
  • Wizard upgrades. Preflight configuration checks, per-type and per-item selection, provider-aware cost estimates, background resume when reopening the tab, a completion screen with per-item errors and one-click retry, and taxonomy terms as an optional batch step.
  • Top-level admin menu with a translation icon.

Improved

  • Massively expanded text coverage: form messages, login strings, image alt text, ARIA labels, FAQ answers, sliders, counters and more, merged into existing installs automatically.
  • Translated posts now carry their source's taxonomy term assignments, so archives and Phox Facets filters work in every language.
  • Post loops and archives are language-scoped on initial render, matching facet refreshes.
  • Global blocks used as post loop card templates swap per language, and internal URL rewiring covers custom link keys such as header logo links.
  • SEO meta, custom fields and WooCommerce translation are now on by default.
  • The Globals tab acts as a one-click repair pass for statuses, references, term assignments and URLs.

Fixed

  • Translated globals no longer disappear from Breakdance's template and popup managers.
  • Copied Breakdance meta (template settings, conditions) survives intact; existing installs are repaired automatically.
  • Translated templates no longer compete with their sources in Breakdance's condition matching.
  • The switcher element renders correctly in the builder when settings change.
  • Overwrite mode re-translates excerpts; the excerpt is no longer skipped.

Security

  • Per-post capability checks on translation endpoints, license checks on all wizard endpoints, server-derived wizard job lists, stronger API-key encryption with per-encryption IVs, and draft URLs removed from the public translation-URLs endpoint.