=== PixelHunter Translate ===
Requires at least: 6.7
Requires PHP: 8.1
Tested up to: 7.1
Stable tag: 0.11.0
License: proprietary

Translate WordPress sites with AI, with dedicated support for WooCommerce.
Content is translated in the background; the front end only reads.

== Description ==

The whole idea of this plugin is one separation: **translation time** happens
in the background, and **serving time** is read-only.

The text of the site is read straight from the database — products,
categories, pages, menus, the shop settings — plus one page of each template,
and the page of each post, page or product you save: what a page builder, a
custom field or a custom tab puts there is translated too, with any builder.
It is sent to the PixelHunter translation service, and from then on the site
serves its pages from a dictionary compiled to a file. A product you publish
is translated a few seconds after your last save. No visitor waits for a
translation, no page waits on an API, and the characters of your license
follow the text you add — not the traffic.

It is the Shopify Translate & Adapt model, and the opposite of the plugins that
translate HTML on demand: those pay for the translation again on every new page
a visitor opens.

== How it is used ==

1. **Languages** — pick the source language (the one the site is written in),
   add the target languages, each with its own URL prefix. A new language
   starts as a **draft**: it is translated, but only administrators can open
   it, and it goes live by itself when its translation is complete.
2. **License** — put in your license key. Then, if you want, a glossary, a
   brand tone and formal or informal for each language under **Settings**.
3. **Dashboard → Translate the site** — reads the text of the whole site and
   sends what is missing to be translated, with a progress bar. You can close the
   page: the rest carries on in the background.
4. From then on, with **automatic translation** on (the default), whatever is
   published or saved is translated on its own, and the whole site is checked
   again once a week and after every theme or plugin update.
5. **Strings** — review and fix by hand. A reviewed translation is never
   overwritten by the machine again.

== The language switcher ==

It is configured on the **Switcher** tab, and it has three modes:

* **Automatic** (the default) — it shows up on its own, floating, without
  touching the theme. You pick the corner. It is what makes the plugin work for
  someone who has just configured a language.
* **Manual** — you place it where you want: the **Language Switcher** block
  (in pages, posts and the Site Editor, also inside a Navigation block), the
  **Language switcher** item in Appearance → Menus, the `[pht_switcher]`
  shortcode in content or a widget, or `<?php pht_switcher(); ?>` in a theme
  template, typically `header.php`.
* **Off**.

There are four shapes — a list, a column, a `<details>` menu and the system
`<select>` — and each one is previewed in the panel with this site's own
languages. You also choose between the language name (*English*, *Español*) and
the code (*EN*, *ES*), and whether a flag comes before it.

The block, the menu item, the shortcode and the function work in all three
modes. In automatic mode the
floating switcher steps aside on any page where you placed one yourself, so a
page never shows two. The floating switcher **does
appear on the cart and on the checkout**: hiding it there was the first
decision, and in practice it did worse, because whoever landed on the cart in
the wrong language was left with no way back to their own, at exactly the
moment they are spending money.

The **Appearance** section of the same tab sets the colours of the box and of
the languages (at rest, on hover and the current one), the text size, the
corners and the shadow, with a live preview next to it. It warns when a text
colour falls under the 4.5:1 contrast WCAG AA asks for.

It is about 4 KB of CSS, injected inline and only on the pages where the
switcher appears. Zero JavaScript in three of the four shapes: they are links,
and a link is the thing browsers and screen readers do best. The `<select>` is
the exception, and it takes eight lines. Everything the Appearance section sets
is a CSS variable on `.pht-switcher` (`--pht-bg`, `--pht-fg`, `--pht-hover`,
`--pht-radius` and the rest), so a theme can still redefine them without
needing `!important`.

== The suggestion bar ==

Whoever arrives in a language that is not theirs gets a small bar at the top
offering the version in their own. It suggests, it does not redirect:
redirecting by browser language breaks the page cache, and Google advises
against it. It shows once — choosing or closing answers for good.

== The license ==

A subscription gives characters every month and a number of sites for one
key: Starter (300,000 a month, 1 site), Pro (1,500,000 a month, up to 5 sites)
or Agency (6,000,000 a month, up to 25 sites), monthly or yearly at the price
of 10 months. Unused characters roll over while the subscription is active.
The free trial gives 30,000 characters for one site, once, with no card. The
key arrives by email after checkout and is always available in your
PixelHunter account, where you can also move it between sites. Changing plan
is done there too: each plan has its own key, so the key of the new plan
arrives by email and replaces the old one on your sites.

The best place for it is `wp-config.php`:

    define( 'PHT_LICENSE_KEY', 'PHT-...' );

That way it is not in the database, it does not show up in an export, and it is
not editable by someone who has the admin panel but not the server. The field
on the License tab is the alternative for whoever cannot touch
`wp-config.php`.

There is no API account or API key to set up: the translation service holds
its own key and picks the model. The plugin never calls the API directly.

Plugin updates need an active license; the free trial counts, with or without
characters left. If the subscription ends, everything already translated
stays online and the plugin keeps working on the version it has; only new
versions and automatic translation stop.

== Recording mode ==

The site is read as an anonymous visitor sees it. That never shows a cart with
items in it, a
checkout returning validation errors, or the message for an invalid coupon —
and that is where the strings that stand out most when left untranslated live.

To catch them: browse the site signed in as an administrator, with
`?pht_record=1` in the address. Whatever shows up is recorded and imported from
the Dashboard. It costs nothing to any visitor, at any moment.

== How it is built ==

* **No rewrite rules.** The language prefix is stripped from the request before
  the WordPress router touches it, so `/en/shop/` resolves like `/shop/` with
  the permalinks the site already has.
* **The core `WP_HTML_Processor`**, the WordPress HTML5 parser. With an empty
  dictionary the output HTML is byte for byte the input — `DOMDocument` lost
  7 KB and corrupted SVG, `<details>` and `<dialog>`.
* **A dictionary compiled to a PHP file**, inside `uploads/`. It sits in
  OPcache: serving a translated page costs neither a query nor an HTTP request.
* **The WooCommerce UI for free.** The `determine_locale` filter makes Woo load
  its own native translation pack — "Subtotal", "Proceed to Checkout", dates,
  currency and notices come translated by humans, at no API cost.
* **No build step, no jQuery, no runtime dependencies.**

Cost measured on the heaviest page of this store (262 KB, 719 replacements):
**~130 ms**, which is `38 ms + 0.12 ms per replacement` — linear, and almost
all of it spent inside the core `set_modifiable_text()`.

In the source language: **0 ms**. The plugin returns before opening the buffer.

Each language is a distinct URL, so the host page cache serves it once per URL
per TTL. On a store with no page cache, count on those 130 ms of TTFB on the
heaviest translated pages.

== Warnings ==

**Page cache TTL under 12 hours.** If the theme prints a nonce on every page —
as a login box in the header does — that nonce is good for 12 to 24 hours. On a
page cached for longer than that, the login fails silently. Each language is a
distinct URL and therefore a distinct cache entry, with exactly the same
restriction.

**The dictionaries are PHP files inside `uploads/`.** The plugin writes an
`index.php` and an `.htaccess` there, but on a server that serves PHP out of
`uploads/` that is not enough. If the host allows it, block PHP execution in
that folder.

== Addresses ==

**Translated addresses.** With the option on (Settings → Translated
addresses), every address is in the reader's language — pages, posts,
products, categories, tags, custom post types and taxonomies, their bases and
the WooCommerce endpoints: `/produto/blusa-de-linho/` becomes
`/en/product/linen-blouse/`, `/minha-conta/orders/` becomes
`/de/mein-konto/bestellungen/`.
That is what Google recommends. It is on by default on new installs; a site
that updates keeps its current addresses until the option is turned on.

* **Nothing breaks.** The source-language address under the prefix redirects
  (301) to the translated one, so links, bookmarks and search results that
  already exist keep working. Turning the option off redirects the translated
  addresses back.
* **An address never changes on its own.** It is created once, from the
  translated title (or from the slug, when someone wrote it by hand), and
  stays. A new translation of the title does not move it.
* **It can be changed by hand** in the Addresses tab. The previous address
  keeps redirecting to the new one, for good.
* No extra characters for most pages: the address comes from the title,
  which is translated anyway. Slugs written by hand are sent as short texts.
* No rewrite rules: the plugin swaps the translated slugs for the source ones
  before WordPress sees the request, and back again in every link.

It works on any site, not only shops. The translated address goes into
every internal link on the page, whoever wrote it: the content, the menus
(also links typed by hand), ACF link fields, the buttons of Elementor,
Breakdance and Oxygen, shortcodes, image maps — and into the canonical, the
hreflang tags, the sitemaps of Yoast SEO, Rank Math and Slim SEO, the
structured data and the REST API. `/%postname%.html` permalinks keep their
extension, and a translated address never takes a word that another plugin
uses as a route.

The plugin learns the real base slugs of the site on source-language requests
and puts them back on any translated request: WooCommerce registers the brand
taxonomy with `_x( 'brand', 'slug', 'woocommerce' )`, which returns `marca` on
a Portuguese site and `brand` on an English one, and the rewrite rules in the
database were written with `marca`.

Some addresses never become HTML and so escape link rewriting: the "Proceed to
checkout" of the blocks comes from a JavaScript configuration object, and the
destination after placing an order comes from the Store API JSON. Those are
prefixed in PHP, through WooCommerce's own filters.

**Date format.** The format stored in the site options usually carries literals
from the source language — `j \d\e F \d\e Y` gives "9 de September de 2026" in
English, half translated. On translated pages the plugin uses each language's
own format (`September 9, 2026`).

== Sitemaps and SEO plugins ==

The plugin has no sitemap of its own, on purpose. The sitemap belongs to the
SEO plugin: it already announces it in `robots.txt`, and the site owner already
submitted it to Search Console. A second sitemap would have to be submitted by
hand, site by site. So the translated pages go into the sitemap that is
already there:

* **Yoast SEO, Rank Math and Slim SEO** — every page gets one `<url>` per
  language, and every `<url>` lists all the versions in
  `<xhtml:link hreflang>`, itself included. That is the format Google documents
  for multilingual sites.
* **No SEO plugin** — the core `wp-sitemap.xml` gets one sitemap per language
  (`wp-sitemap-translations-en_posts_page-1.xml`). The core accepts nothing but
  `loc` and `lastmod`, so there the translations go in as plain addresses; the
  `hreflang` is already in the `<head>` of every page.

On translated pages the canonical URL and `og:url` keep the language prefix
with any of the three SEO plugins. A canonical pointing at the source page
tells Google the translation is duplicate content, and it gets dropped from
the index.

**Structured data.** The JSON-LD that WooCommerce and the SEO plugins put on
every page — the product name and description, the breadcrumb, the page
title — is what Google shows in its results. On translated pages it uses the
same translations as the page, and the site's own addresses inside it get the
language prefix. It uses no characters: those texts are the page's own. A text
that exists only in the JSON-LD, and on no page, stays in the source language.

Rank Math keeps its sitemap cached for up to 100 days, and Yoast can be told to
cache it too. Saving the languages clears both caches, so a new language shows
up in the sitemap right away.

== Updates ==

The plugin updates itself from PixelHunter's update server, not from
WordPress.org: new versions show up in Plugins → Installed Plugins, with
"View details" showing this changelog. The `Update URI` header keeps
WordPress.org from ever offering a plugin with the same slug over this one.

The update server is a Cloudflare Worker whose address only exists inside the
plugin; the source code and the archive of every version live in private
repositories. No token or password ships inside the plugin.

Every package is signed with an ed25519 key, and the plugin checks the
signature before WordPress installs anything. A package that was swapped on
the server, or published by someone without the private key, is refused.

**To publish a version:** raise `Version` in `pixelhunter-translate.php`, the
`VERSION` constant and `Stable tag`, write the `= X.Y.Z =` section of the
changelog, push, and create a GitHub release with the tag `vX.Y.Z`. The
release workflow builds, signs and publishes it, and refuses to when any of
those do not match. Sites see it within 12 hours, or at once with
Dashboard → Updates → Check again.
The full step by step, with what each error means, is in `RELEASING.md` in
the source repository.

A copy of the plugin that is a git checkout never updates itself: the
installer would replace the folder, repository included.

== Known limitations ==

* **Addresses outside a link stay in the source language.** An address in a
  `data-url` attribute, in a script's configuration or in the value of a
  dropdown that JavaScript follows is not translated. It still works: the
  source-language address always redirects to the translated one.
* **Asynchronous emails go out in the source language.** An email triggered by
  a status change in the admin has no request to inherit the language from.
* **One string, one translation, across the whole site.** "Free" is always
  translated the same way, wherever it shows up. The "Where" column in the
  table gives the reviewer the context. It is the same limitation Shopify has
  for theme strings.
* **It does not translate inside `<script>`,** apart from the JSON-LD
  structured data (see "Sitemaps and SEO plugins"). The rest is code.
* **Builder text that was never read shows once in the source language.** On
  a page made before this version and not saved since, the page is read when
  someone opens it translated: that first visitor sees the builder's text
  untranslated, the next ones do not.
* **A header or footer made in a page builder** is read by the weekly check
  or by Translate the site, not right after it is saved.
* **Text a page only shows after it loads, through JavaScript,** is not read.

== Translation packs: the half you do not pay for ==

A translated site has two sources of text, and neither replaces the other.

The **software interface** — "Subtotal", "Proceed to checkout", "Add a coupon",
error notices, dates, currency — comes from the official WordPress.org
translation packs. They are reviewed by humans and cost nothing. The **content
of the site** — product names, page text, menus, theme strings — comes from
this plugin's dictionary, translated once and served from a file.

This plugin changes the `locale` of the request. From there on, WordPress and
WooCommerce go looking for the translation files of that locale — and if they
do not find them, they serve the originals, which in WooCommerce are English.
The symptom is a Spanish site with the cart and the checkout in English, with
no error anywhere explaining why.

That is why the panel says which packs are missing and installs them with one
button. It takes a few seconds, and it avoids sending the API thousands of
interface strings that already exist translated, and translated better.

**Two notes for whoever works on this:**

* Packs are **not** installed when a language is saved, and that is deliberate:
  installing one downloads from WordPress.org and runs a bulk upgrade, and two
  languages added at once killed the request on `max_execution_time` — after
  the option had already been written, so the languages looked configured and
  nothing translated, with no error to explain it. The button does it one
  language at a time and reports what failed.
* Installing the core pack is not enough. The WordPress `bulk_upgrade()` does
  not look for what exists — it reads the update transients, and those were
  built when the new locale was not yet installed. Without forcing a check
  first, the core ends up translated and WooCommerce ends up with no
  translation at all.
* If the automatic install fails — a server with no route to the internet, or
  `wp-content/languages` with no write permission — the panel says which ones
  are missing and shows the WP-CLI commands that fix it.

== The cart and the checkout ==

They are blocks, and their content comes as JSON from the Store API — which the
HTML buffer does not see and should not see. They are covered, in three
different ways:

* **The labels** ("Subtotal", "Proceed to Checkout", dates, currency, notices)
  come from the native WooCommerce translation pack, reviewed by humans. Zero
  API cost.
* **The content** — product, variation and category names — is translated by
  the dictionary. A request to the Store API carries no language prefix at all,
  so the language is read from the `Referer`, validated against the site domain.
* **The text the store wrote** — shipping method names, payment method titles
  and instructions, attribute labels — is collected straight from WooCommerce
  when you click Detect. It shows up on no page the detector visits: an empty
  cart shows no shipping methods, and an empty checkout redirects.

Two notes for whoever works on this:

* The WooCommerce blocks keep visible text in `data-content`, on an empty
  element that JavaScript fills in later. That attribute is translated; the
  `data-wp-*` of the Interactivity API are **not**, because they hold code
  expressions.
* An order is always saved in the source language, even when it is placed in
  another one: the admin, the invoices and the reports read one language, and
  the translation happens on the way out. Only presentation filters translate
  order data — WooCommerce saves each item name through its `view` getter, so
  a filter there would write the translation back into the database.
* WooCommerce keeps the calculated shipping methods in the session and only
  recalculates them when the cart changes. The plugin adds the language to the
  "package", so each language gets its own entry instead of inheriting the
  other one's.

== Changelog ==

= 0.11.0 =
* **The glossary now reaches what is already translated.** Until now it only
  applied to translations made after saving it, so a term you fixed kept the
  old wording everywhere it had already been translated. When you change the
  glossary, the brand tone or formal/informal, Settings tells you how many
  saved translations use what changed and offers to translate them again.
  Nothing is sent without you asking: translating again counts against the
  characters of your license.
* Translations you reviewed by hand are never redone, and the site keeps
  showing the current text until each new translation arrives — no page falls
  back to the source language meanwhile.

= 0.10.2 =
* **Edit translations shows where each text is.** The page marked only about
  one text in ten — the rest are in a closed menu, hidden, or an attribute
  such as `alt` — so it looked as if nothing happened. The mark now goes only
  to what you can see, a text you can't see says **Not visible** next to its
  status, and the mark stays while you look at the page with the field
  still selected.
* **The editor title looks right on Breakdance sites.** It took the site's
  heading style — huge, in capitals, dark on the dark header.
* **The language suggestion bar shows the flag.** It showed a globe; now it
  shows the language's flag, drawn the same on Windows and Mac, when Flags is
  on in the switcher.

= 0.10.1 =
* **The sitemap shows the translated addresses right away.** With Rank Math
  or Yoast SEO, turning on Translated addresses left the old addresses in the
  sitemap for up to 100 days, each one a redirect in Search Console. The
  sitemap is now refreshed whenever the addresses change, and once when you
  update.

= 0.10.0 =
* **Translated addresses.** Every address can be in the reader's language —
  pages, posts, products, categories, custom post types and taxonomies, their
  bases and the WooCommerce endpoints (`/en/product/linen-blouse/`,
  `/de/mein-konto/bestellungen/`), turned on in Settings → Translated
  addresses — on by default on new installs. The old addresses redirect (301)
  to the new ones, an address never changes on its own, and each one can be
  edited in the new Addresses tab.
* **Close languages get a real translation.** Between languages as close as
  Portuguese and Italian, a short text could come back unchanged, as if it
  were already translated.
* **Password reset works in every language.** On a translated page, the
  WooCommerce reset link sent the customer back to "Lost your password?"
  instead of the form for the new password.
* **The home page gets its languages in the Rank Math sitemap.** It was the
  only page there without its translated versions and hreflang.
* **The order received page stays in the customer's language.** After an
  order placed on `/en/` with bank transfer, the customer was sent to the
  order received page in the source language, at an address with two slashes.

= 0.9.1 =
* **The Edit translations panel scrolls again with smooth-scrolling
  themes.** On themes that use smooth scrolling (Lenis), the mouse wheel over
  the panel scrolled the page behind it instead of the list of texts. The
  list now scrolls, and reaching its end no longer moves the page.

= 0.9.0 =
* **New languages start as drafts.** A language added in Languages is
  translated right away, but only administrators can open it, and it stays
  out of the switcher, the hreflang tags and the sitemap until its
  translation is complete — then it goes live by itself. Google no longer
  indexes pages that are half translated, for instance when a free trial
  ends halfway. Visitors who open a draft address go to the same page in the
  source language. The languages you already have stay published.
* Each language in Languages has a **Published / Draft** choice and a
  **Preview** link. Publishing or unpublishing clears the page cache, since
  every page lists the languages.
* Every section of the panel has the same header: the dark band the License
  card already had.
* **Structured data in the visitor's language.** The JSON-LD that Google
  reads — the product name and description, the breadcrumb, the page title,
  from WooCommerce and the SEO plugins — stayed in the source language on
  translated pages, so a search result could show a product in Portuguese on
  the English site. It now uses the same translations as the page, and its
  addresses point to the translated pages. It uses no characters.
* **Flags that show on Windows.** The switcher's flags were emoji, and
  Windows has no flag emoji: it showed the letters *PT* instead. They are now
  drawn inside the page, only for the languages you have, and look the same
  on every system — still with no images and no extra requests. The System
  box keeps the emoji, since an option can only hold text.
* **Database changes arrive with the update.** The plugin's tables were only
  created or changed when the plugin was activated, and an update does not
  activate it again. Now each site brings its tables up to date the first
  time it loads a new version. On a multisite network, sites other than the
  main one get their tables too — before, they had none.
* **You hear about it when translation stops.** Before, running out of
  characters or a refused license only showed on the plugin's Dashboard, and
  new products stayed in the source language without anyone knowing. Now a
  notice appears at the top of the WordPress admin while translation is
  stopped, and the site's admin email gets one email — not one every hour.
  With less than 10% of the plan left, a notice shows on the Dashboard and
  Plugins screens, and one email is sent.
* **Formal or informal, per language.** Settings has a new Per language
  table: choose *Sie* or *du* for German, *vous* or *tu* for French, *usted*
  or *tú* for Spanish, and every translation in that language uses it. Before,
  the same page could mix both. Each language also gets its own glossary,
  for translations that only fit that language. With nothing chosen,
  translation works as before.
* **Better translations of short texts.** Buttons, menu items, headings and
  shipping methods are now translated knowing where they appear, so
  *Início* in a menu becomes *Home*, not *Start*, and menus and buttons keep
  the capitalization of the original instead of English Title Case. The
  translation also knows your site's name and tagline (Settings → General),
  instead of treating every site as a store. It uses no extra characters.
* Translations into English (United States) now say which English they
  are, as the other languages already did: product words come out American
  (*sweater*, *pants*), not British (*jumper*, *trousers*).
* **Keep a block in the original language, without code.** In the block
  editor, write `notranslate` under Advanced → Additional CSS class(es), and
  that block — with everything inside it — is never translated. Page
  builders work the same, in their CSS class field. Before, it took the
  `translate="no"` attribute, typed into the HTML. Text that already has the
  `notranslate` class, which Google Translate also respects, now stays in the
  original language too.
* **The switcher as a block and as a menu item.** In block themes, add the
  **Language Switcher** block to the header — also inside a Navigation block,
  next to its links — or anywhere in a page. Its sidebar picks the shape, name
  or code, and flags, or follows the Switcher tab. In classic themes,
  Appearance → Menus has a **Language switcher** box: the item becomes one
  link per language, drawn by the theme like the other items, with the other
  languages in a submenu when the shape is Menu. Either way, the floating
  switcher steps aside on that page. Before, it took the shortcode or PHP.
* The Menu shape no longer stretches across the page when it is placed in
  content: the button is as wide as the language name.
* **Edit translations on the page itself.** Logged in as an administrator,
  pick **Edit translations** in the toolbar of any page, and a language. A
  panel next to the page lists that page's texts in the order they appear:
  the original above, the translation below. Point at one and the page shows
  where it is; change it, press **Save**, and the page updates straight away.
  It works for draft languages too. Before, fixing a translation meant
  finding it among all the site's texts in Strings.
* A translation typed by hand with an ampersand (&) showed a code on the
  page instead of it. It now shows as typed.
* **Visitors see your corrections.** A translation fixed by hand, in Strings
  or on the page, now clears the page cache a minute later. Before, you saw
  the fix — logged-in users skip the cache — while visitors kept the old
  translation until the cache expired.
* **Text from page builders, custom fields and custom tabs.** Pages and
  products made with Elementor, Breakdance, Oxygen, Bricks or any other
  builder, fields from ACF and similar plugins, and custom product tabs keep
  their text outside the fields the plugin reads from the database. That text
  stayed in the source language, with no warning, unless the page was one of
  the few the weekly check opens. Now the plugin also reads the page itself:
  after you save, and when someone opens a translated page with text it has
  not read yet. It works the same with any builder. Two saves in a row — a
  typo fixed right away — are read once, a few seconds after the last one.
* The plan cards in **License** show the current prices: Starter $9, Pro
  $19 and Agency $49 a month, or $90, $190 and $490 a year. If you already
  subscribe, you keep the price you have.

= 0.8.13 =
* **FlyingPress support.** When new translations arrive, the plugin now
  clears the FlyingPress cache of the translated pages, as it already did
  for LiteSpeed Cache, WP Rocket and W3 Total Cache. Visitors see the new
  translation right away instead of waiting for the cache to expire.

= 0.8.12 =
* **The plugin stays active after an update.** On some servers WordPress
  deactivated the plugin right after updating it, even with the files in
  place, and the translated pages returned "not found" until someone
  activated it again. The plugin now puts itself back on the list of
  active plugins when WordPress removes it silently and its files are fine.
  Deactivating it by hand works as before.
* **Diagnostic info for support** shows when that happened and what the
  plugin saw at the time, so the cause on each server can be found.

= 0.8.11 =
* **Quotes no longer hide a translation.** On a translated page WordPress
  writes the quotes of that language („…“ in German, « … » in French and
  Spanish), so a sentence with quotes stopped matching its translation and
  stayed in the original language. The plugin now finds it whatever the
  quotes.
* **Translations always reach the site.** If a translation run is cut short
  (a plugin update, an error, a server restart), the dictionary of that
  language is rebuilt on the next run, instead of waiting for the next
  change.

= 0.8.10 =
* **The site name is never translated.** The name set in Settings → General
  now goes to the translation API as a brand to keep as it is. Names that
  look like ordinary words, such as "PixelHunter Translate", used to come
  out half translated in page titles, in `og:site_name` and in meta
  descriptions.

= 0.8.9 =
* **Refunds, explained as they work.** The Help tab now says that a refund
  covers the characters not yet used and ends the subscription, and that
  the key stops working within the hour. It used to say only that a refund
  revokes the key.

= 0.8.8 =
* **The page title is translated with an SEO plugin too.** With Rank Math
  or Yoast, the `<title>` of a translated page (the one Google shows in its
  results) stayed in the original language, while `og:title` and the meta
  description were already translated. It now uses the same translation.

= 0.8.7 =
* **Terms and privacy next to the plans.** Below the plan cards, the
  License tab now says that starting the free trial or subscribing means
  agreeing to the Terms of Service and the Privacy Policy, with a link to
  each on pixelhuntertranslate.com.
* **A busy license server no longer looks like an empty balance.** When the
  license server is busy for a moment, the plugin now says so and tries the
  batch again, instead of showing "no characters left" or "license not
  active" to a customer who still has characters.

= 0.8.6 =
* **The Help tab starts with this site.** How many characters the text
  detected on the site adds up to, what translating it into your languages
  uses, how much is still to translate, and a warning when that is more than
  the characters left.
* **Every Help section links to the documentation** on
  pixelhuntertranslate.com, where the full text lives.
* **"What does this mean?"** next to an error on the Dashboard opens its line
  in the documentation's table of messages, with what to do about it.
* **Diagnostic info for support.** The Help tab shows what support needs to
  know (plugin, WordPress and PHP versions, plan, balance, languages, the
  last error), with a button to copy it and one that opens an email to
  support with it already filled in. It never includes the full license key.
* Support is now support@pixelhuntertranslate.com, and the plugin's site is
  pixelhuntertranslate.com.

= 0.8.5 =
* **An up-to-date translation template.** The `.pot` file in `languages/`
  now has every string of the panel as it is today, for anyone translating
  the plugin into another language. It had not been regenerated since 0.3.0.
* Internal cleanup of the code that talks to the translation API. Nothing
  changes in how the plugin works.

= 0.8.4 =
* The plugins list no longer shows "View details" twice. WordPress adds its
  own link once it has checked for updates, and the plugin now only adds
  one when WordPress has not.

= 0.8.3 =
* **New colours.** The plugin panel, its menu icon and the banner follow the
  new PixelHunter brand: a dark grid with a cyan pixel.
* **A fuller Help tab.** How yearly plans are credited (all twelve months at
  the start), the free trial rules (one per site, a real email address),
  translating by hand without a license, why deleting the plugin does not
  free its site or cancel the subscription, why plan changes go through My
  account and not the License tab, and what keeps working after cancelling.
* Clearer messages when the translation API is busy or unavailable: they
  say the batch will be retried in a moment, so there is nothing to do.

= 0.8.2 =
* **Lighter on the site while it translates.** Languages are translated one
  at a time instead of all together, at the same speed: each one is
  complete sooner, and visitors no longer see several languages half done.
* The translation files are rewritten at most once a minute while a
  language is in progress, instead of after every step. That was what made
  the site slower during a large translation.
* Translate the site shows which language it is on ("Translating German,
  2 of 6"), and says the site may be a little slower until it finishes.
* **Monthly or yearly on the License tab.** A switch shows each plan's
  yearly price and its characters for the whole year, credited at the
  start, and Subscribe opens the checkout on the yearly plan.

= 0.8.1 =
* Clearer help on changing plan: each plan has its own license key, so the
  key of the new plan arrives by email and has to be pasted on your sites.
* The License tab warns subscribers to change plan in My account instead of
  subscribing again, so nobody is charged twice.
* The automatic translation notice points to the Settings tab, where the
  option is.

= 0.8.0 =
* **Updates need an active license.** New versions download only with a
  valid license key, the free trial included. If the subscription ends,
  what is already translated stays online and the plugin keeps working on
  the version it has. A refused update says why on the Updates screen.

= 0.7.2 =
* An internal document of PixelHunter shipped inside the plugin folder in
  0.7.0 and 0.7.1. It carried no keys or secrets. It is removed on update.

= 0.7.1 =
* The Save button of the License tab sits next to the key field. It was at
  the bottom of the page, under the plans, where nobody looked for it.
* The plan cards lead with the price. Characters and sites come below, on
  the same lines in every card; the Pro card is the dark one.

= 0.7.0 =
* **Plans instead of top-ups.** Starter, Pro and Agency: characters every
  month and 1, 5 or 25 sites on one key, monthly or yearly. Unused characters
  roll over while the subscription is active. The License and Top-up products
  are no longer sold; keys already bought keep working with their balance.
* **One key, several sites.** A Pro or Agency key activates on each site that
  uses it, up to the sites of its plan. The top of every screen shows the
  plan and the sites in use, as in "Pro · 3 of 5 sites". Free a site from
  your account when you need the slot.
* **The balance is refreshed when you open the plugin,** not only after a
  translation. With a key shared by several sites, a quiet site showed an
  old number.
* Help and License tabs rewritten for the plans.

= 0.6.0 =
* **A license key instead of an API key.** The plugin no longer calls
  the API directly. Text is sent to the PixelHunter translation service, which
  checks your license, counts the characters and translates. There is no API
  account to set up and no model to pick.
* **Characters, not tokens.** Every license has a balance of characters. It
  shows at the top of every screen, with a bar. When it runs out, translation
  pauses and resumes on its own after a top-up.
* **One key per site.** A key activates on the first site that uses it. Move
  it to another site from your account, without asking anyone.
* **Translation → License.** Paste the key, see the balance, buy a license or
  a top-up. A free trial with 30,000 characters needs no card.
* **Help tab.** How the license works, moving it between sites, balance and
  top-ups, invoices and support.
* Sites that had an API key stop translating until a license key is
  entered. What is already translated keeps being served.

= 0.5.4 =
* On the Strings tab, the translation boxes no longer carry a coloured
  edge for their status. The status is already in the filter above the
  list; the edge was noise.

= 0.5.3 =
* **Order emails in the customer's language.** The emails sent after the
  purchase — paid, completed, refunded, a note from the shop — went out in
  the store's language, whatever language the customer had bought in. They
  now go out in the language of the purchase, from the subject to the
  footer: product, shipping and payment names, the date, and the text you
  wrote for the emails (subject, heading, additional content, footer).
  Emails to the shop administrator stay in the store's language.
* The plugin's own mark in the admin menu, in the brand colours.

= 0.5.2 =
* **A new look for the panel.** A header with the plugin's mark and the
  version, each subject in its own section, tables and forms with the same
  spacing and borders on every tab, and buttons in the brand colour. Only
  the plugin's own page changes: the rest of the admin and the site are
  untouched.
* On the Switcher tab, the settings are split into sections: where it
  appears, shape, how it reads, appearance, and placing it by hand.

= 0.5.1 =
* In automatic mode, the floating switcher no longer shows on a page where
  you placed one yourself with `[pht_switcher]` or `pht_switcher()`. Those
  pages showed two switchers; now they show only yours.
* On the Switcher tab, the previews change as soon as you pick the flags or
  the name/code option. Before, nothing changed until you pressed Save, and
  it looked as if the option did not work.

= 0.5.0 =
* **Nothing is left untranslated any more.** The detector only visited 40
  products and 40 categories of each kind: in a shop with 454 products, 414 of
  them — and 172 categories — were never translated, and the Dashboard still
  said there was nothing left to do. The whole site is now read, and the first
  check after this update translates what was left behind.
* **Automatic translation.** A product, page or category is translated a few
  seconds after it is published or saved, in the background. The whole site is
  checked again once a week and after every theme or plugin update. It is on
  by default and can be turned off in the Translation tab.
* **Much faster, and the site stays fast while it works.** The text is read
  straight from the database instead of opening every page of the site, and
  several batches go to the API at the same time. A whole shop is translated in
  under a minute, where it used to take over half an hour and slow the site
  down for whoever was browsing it.
* **About 60% fewer strings to pay for.** Sentences that join a name to
  template text — "/ Dresses", "Be the first to review “Summer Dress”", "Black,
  Blue", "5 items" — are now assembled from their translated pieces when the
  page is served, instead of being translated one by one for every product.
  This also fixes product names left in the source language inside
  WooCommerce sentences.
* **One button.** The Dashboard has a single **Translate the site** button with
  a progress bar, instead of Detect and Translate for each language.
* **Nothing gets stuck.** A text the API cannot translate is marked as Failed
  (Strings → Failed, to write by hand) and no longer blocks the rest. A wrong
  key or an account without credit pauses for an hour, with the error shown on
  the Dashboard.
* After translating, the translated versions of the pages that changed are
  cleared from the page cache (WP-Optimize, LiteSpeed, WP Rocket and W3 Total
  Cache).

= 0.4.1 =
* Updates come from PixelHunter's own update server instead of a public
  repository, so the plugin can only be downloaded by whoever already has it.
  0.4.0 was never distributed.
* A "View details" link next to the plugin, in Plugins → Installed Plugins,
  opens the description and this changelog at any time — not only when an
  update is waiting.

= 0.4.0 =
* **Automatic updates.** New versions show up in Plugins → Installed Plugins
  like any other update, and install with one click or on their own. Every
  package is signed, and the plugin refuses to install one whose signature
  does not match. This is the first version that updates itself: sites on an
  older version need this one installed by hand, once.
* The Switcher tab has an Appearance section: colours of the box and of the
  languages (at rest, on hover, current), text size, corners and shadow, with a
  live preview and a contrast warning. The defaults are the colours the
  switcher always had.
* In the bottom corners, the list and the column sat about 4px off centre in
  their box: a margin meant for the menu, which opens upwards there, was
  landing on them too.
* Orders placed in a translated language saved the product, shipping and
  payment names in that language. The dictionary is keyed by the source text,
  so from then on no language translated them — an order placed in Spanish
  stayed in Spanish in English, in German and in the admin. Orders now save
  the source names, and the language of the purchase in `_pht_lang`; the
  order pages and the email sent at checkout translate them on the way out.
* The notice that a product has been added to the cart kept the product name
  in the source language: WooCommerce builds it from `get_the_title()`, which
  skips the product filters. The name is now translated when the notice is
  created.
* Addresses are no longer translated. The API turned "rua" into "calle" in the
  shop's own address, and that entry then reached every page where the same
  address showed up. The prompt now tells the model to leave addresses, names,
  emails, phones and tax IDs exactly as written, and text inside `<address>`
  is never translated.

= 0.3.0 =
* Translated pages are now in the XML sitemap, with `hreflang` alternates:
  inside the Yoast SEO, Rank Math and Slim SEO sitemaps, or as one sitemap per
  language in the core `wp-sitemap.xml` when there is no SEO plugin.
* With Slim SEO active, the canonical URL of a translated page pointed at the
  source page. It now keeps the language prefix, as it already did with Yoast
  and Rank Math. `og:url` keeps it too, with all three.
* On a site installed in a subfolder (`example.com/shop/`), source-language
  pages sent the `hreflang` tags and the language switcher to addresses with
  the folder twice (`/shop/shop/…`), all of them 404. Translated pages and
  sites at the domain root were not affected.
* English is now the plugin's source language. Every string in the admin
  interface, in the front-end switcher and in the error messages was rewritten
  in English, and the `.pot` was regenerated from them. A site that wants the
  panel in another language translates it like any other plugin.
* The system prompt sent to the API is in English too. The source and target
  languages were already going in by their English names, so a Portuguese
  prompt naming "Spanish (Spain)" was mixing two languages in one instruction.
* The readme was rewritten in English, and two claims in it that were no longer
  true were corrected: the floating switcher does appear on the cart and on the
  checkout, and translation packs are installed by the panel button, not when a
  language is saved.

= 0.2.1 =
* Cleanup: excess code goes, without changing what the plugin does. One shared
  string translation instead of a copy in `Woo` and another in `Json`; the REST
  routes in a table instead of ten identical blocks; one `asset_version()`
  instead of three; the `/status` route, which nothing called, gone.
* The panel stops loading `wp-api-fetch` (and `wp-polyfill` behind it): the
  plugin JavaScript never used it.
* Uninstalling now also deletes the `pht_lang_locales` and `pht_source_slugs`
  options, which were left behind.

= 0.1.0 =
* First version.
