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.
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
| Requirement | Minimum version |
|---|---|
| WordPress | 6.0 or newer |
| PHP | 7.4 or newer |
| Breakdance, Oxygen or Bricks | Optional, recommended. Tested with Breakdance 2.8, Oxygen 6.1 and Bricks 2.2 |
| WooCommerce | Optional. Tested against WooCommerce 10.6 and 11.1 |
| A DeepL or OpenAI account | For the API key |
- Install the plugin. In WordPress go to Plugins → Add New → Upload Plugin, choose the
phox-translate.zipfile and click Install Now. - Activate it. A new Phox Translate item appears in the main admin menu.
- Add your API key. Open Phox Translate → Provider, choose DeepL or OpenAI, paste your key and click Test Connection.
- Choose your languages. On the General tab set your default language and tick your target languages.
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.
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.
- 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.
- Open any page in the WordPress editor. In the sidebar you'll find the Phox Translate box listing your target languages.
- 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.
- 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. - 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.
Getting startedThe admin interface
Everything lives under the Phox Translate top-level menu, organised into seven tabs:
| Tab | What you configure |
|---|---|
| General | Default language, target languages, URL style, the language-root homepage, the untranslated-content fallback, which post types are translatable, and flag overrides for the switcher. |
| Provider | DeepL or OpenAI, your API key and a connection test. |
| Globals | Headers, footers, global blocks, templates and popups, with per-language status and one-click translate buttons. |
| Wizard | The site-wide translation flow: audit, options, confirm, progress. |
| Strings New | WooCommerce product attribute labels per language, edited by hand or translated in one click. Hidden when WooCommerce isn't active. |
| Advanced | The text-key whitelist, excluded terms, HTML preservation, auto-linking, the flag font, and the SEO / custom fields / WooCommerce toggles. |
| License | Activate 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
| Style | Example | How it works |
|---|---|---|
| Prefix (default) | /fr/about | Hierarchical 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-fr | The 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.
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.
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.
SetupTranslation providers
You bring your own API key and pay the provider directly. Typical cost is pennies per page.
| Provider | Best for | Notes |
|---|---|---|
| DeepL | European languages, best-in-class quality | Free keys (ending :fx) and Pro keys are auto-detected and routed to the right endpoint. The free tier includes 500,000 characters per month. |
| OpenAI | Broadest language support | Uses 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.
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.
SetupLicense
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.
TranslatingSingle 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.
TranslatingHeaders, 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.
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.
- 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.
- Select. Untick any content type, or expand a type to exclude individual pages. Taxonomy terms and menu labels have their own include toggles.
- 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.
- 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.
- 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.
- 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_queuedatabase 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.
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.
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.
Nav menus
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.
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 sees | Where it comes from |
|---|---|
| Title, description, attribute labels and values | The translated product |
| Variation dropdowns / swatches, images, prices, stock | The source product's variations |
| Cart and checkout line names, and the "added to cart" notice | The translated product — including the block cart and checkout |
| The line item's link back to the product | The translated product's URL |
| Stock decrement, order totals, emails, reports | The 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.
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.
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:
- 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.
- Edit by hand. Change any field and save. Leave a field empty to show the original label in that language.
- 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.
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.
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.
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:
| Method | Where |
|---|---|
| Builder element | Search "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 tag | pt_language_switcher() in theme files. |
Options
| Option | Values | Shortcode attribute |
|---|---|---|
| Display style | Dropdown, inline links, flags only, flags + text | style="dropdown" |
| Flag style | Rectangle, circle or none | flags="rectangle" |
| Show current language | On / off | show_current="1" |
| Untranslated pages | Link to that language's homepage, or hide the language | missing="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"]
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_filterquery 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.
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.
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, plusx-defaultpointing 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
langattribute. 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
| Setting | Purpose | Default |
|---|---|---|
| Text keys to extract | The 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 terms | Brand 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 HTML | Keep inline markup intact during translation. | On |
| Auto-link new pages | Create the language group link automatically when a new page is translated. | On |
| Translate SEO meta | Yoast / RankMath titles and descriptions. | On |
| Translate custom fields | ACF and Phox Studio fields, including repeaters and groups. | On |
| Translate WooCommerce | Product attributes and purchase notes. | On |
| Load the bundled flag font | Only 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.
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.
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
| Phase | Behaviour |
|---|---|
| DNS / connect | 30-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. |
| Transfer | 180-second request timeout, retried once. A long request has probably already cost provider time, so it isn't hammered. |
| Unreadable response | If 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.
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.
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.
Developer reference
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
| Where | Key | Holds |
|---|---|---|
| Any post | _pt_language | The post's language code (set on sources once they have a translation). |
| Source post | _pt_translations | Map of lang => post ID for its translations. |
| Translated post | _pt_source | The source post's ID. |
| Translated global | _pt_is_global_translation | Marks headers/footers/templates/popups/blocks so they're excluded from the builder's condition matching. |
| Hub page | _pt_language_hub | Marks the hidden per-language URL parent. |
| Translated post | _pt_source_hash · _pt_stale | Staleness baseline and the denormalised comparison result (so the admin filter is indexable). |
| Source post | _pt_source_hash_cache | Fingerprint + hash pair, so recomputing staleness in a list table stays cheap. |
| Order line item | _pt_lang | The 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
| Kind | Name | Holds |
|---|---|---|
| Taxonomy | pt_language | Hidden 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. |
| Option | phox_translate_settings | All plugin settings (the API key inside is encrypted). |
| Option | pt_attribute_labels | [ lang ][ taxonomy ] => label for the WooCommerce attribute-label overlay. |
| Option | phox_translate_db_version · phox_translate_migration_state | The migrations runner's version number and crash sentinel. |
| Option | pt_lang_tax_ready · pt_markers_ready | Gates: the language index and the untranslated-content markers are only trusted once their background backfill has completed. |
| Table | {prefix}pt_batch_queue | One row per queued translation job. |
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.
| Method | Route | Access | Purpose |
|---|---|---|---|
POST | /translate | edit_post on the target | Translate one post into one or more languages: { post_id, languages: [] }. |
POST | /wizard-audit | Admin + license | Run the site audit and return the manifest. |
POST | /wizard-start | Admin + license | Queue a batch. Items are derived server-side; the client sends only languages, options and exclusion filters. |
GET | /wizard-progress | Admin + license | Report batch progress; each poll also processes one queued job. |
GET | /batch-errors | Admin | The recent batch error log. |
GET | /diagnose/{post_id} | Admin | Dump a post's builder data structure and exactly what the parser extracts. Invaluable when tuning text keys. |
GET | /translation-urls/{post_id} | Public | Published 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
| Var | Effect |
|---|---|
pt_filter_lang | Set on a custom WP_Query to opt in to language filtering. |
pt_skip_lang_filter | Set 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. |
Help
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).
HelpFAQ
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.
HelpChangelog
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_typeterm 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_productaction reporting everything that decides how a translated product behaves — post type and status, source link,product_typeterms 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_visibilityandpost_formathold 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_menumenus 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 andpt_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.
