WP Manifestindependent plugin directory
manifest / i18n / open-tongue-translations

Open Tongue Translations

Open Tongue Translations connects WordPress to a self-hosted [LibreTranslate](https://libretranslate.com/) instance — running on the same server, over a Unix socket, or on a private VPC network — so that every translation request stays entirely within your infrastructure.

by tporret · github.com/tporret/open-tongue-translations · website

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/tporret/open-tongue-translations/archive/refs/heads/main.zip

Readme

Open Tongue Translations

A privacy-first WordPress translation plugin. Core guarantee: no translation data ever leaves the host server.


Description

Open Tongue Translations connects WordPress to a self-hosted LibreTranslate instance — running on the same server, over a Unix socket, or on a private VPC network — so that every translation request stays entirely within your infrastructure.

Zero external calls. Zero cloud dependencies. Zero data leakage.


How it works

The plugin intercepts translatable content at two independent layers:

  1. Gettext Interception — hooks into WordPress's own translation pipeline (gettext filter) to translate strings that pass through __(), _e(), and related functions.
  2. Output Buffer Interception — opens an output buffer on template_redirect, captures the full rendered HTML, extracts visible text nodes in a single batched API call, and re-injects the translated text before the response is sent.

Together these layers provide complete coverage: strings loaded from .mo files and strings printed directly by themes or plugins.


Persistence and caching

Translations are stored locally in a dedicated MySQL table ({prefix}libre_translations) and served through a two-level read-through cache:

  • L1 — Object Cache (wp_cache_*) uses Redis, Memcached, APCu, or a per-request in-memory array on sites without a persistent drop-in. Batch lookups use a single MGET round-trip — never a loop.
  • L2 — Database falls back to a single SELECT … IN (…) query when L1 misses. L2 hits are backfilled into L1 automatically.

Result: O(1) for cached strings. The translation API is only called on a cold miss.

Human-edited translations are protected by an is_manual flag. API re-translations can never overwrite a row where is_manual = 1, enforced atomically at the SQL level.


Static-cache compatibility

When WP Rocket or Cloudflare caches a translated page as static HTML, the plugin automatically purges those caches whenever a translation changes:

  • WP Rocket — calls rocket_clean_post() per-post or rocket_clean_domain() on a full locale flush.
  • Cloudflare — supports both the Cloudflare WordPress Plugin (via action hooks) and direct API calls (via Zone ID + Bearer token credentials stored in options).

Both layers can be active simultaneously.


Automatic maintenance

A WP-Cron job (ltp_weekly) prunes translation rows unused for more than 90 days (configurable via ltp_prune_days). Deletions are batched at 1,000 rows per query to avoid table locks. Human-edited rows are never pruned.


Admin settings page

All plugin options are configurable via the Open Tongue top-level admin menu. The settings page is organised into five tabs:

Tab Contents
Dashboard System health overview: DB tables, API reachability, L1/L2 cache status, WP-Cron schedule, Privacy Status badge
Translation Engine ltp_target_lang select (populated from the live LibreTranslate /languages endpoint); ltp_detect_browser_locale checkbox
Connectivity Radio selector for ltp_connection_mode (localhost / socket / vpc) with JS-toggled conditional fields; Privacy Guard banner
Exclusions & Glossary Full CRUD for {prefix}ott_exclusion_rules — CSS, XPath, and Regex rule types; scope to global / post type / post ID
Performance ltp_prune_days; WP Rocket and Cloudflare compat toggles with inactive-plugin notices; L1/L2 status cards

A Privacy Guard banner is rendered below the page heading. It shows green for localhost / socket modes or any RFC 1918 VPC IP, and red for publicly-routable VPC addresses with a direct link to the Connectivity tab.

All forms use settings_fields() / check_admin_referer(). All output is escaped (esc_html, esc_attr). All inputs are sanitized via typed callbacks registered with register_setting.


Language routing and browser detection

OTT_Language_Service fetches the list of supported languages from the configured LibreTranslate driver and caches the response for 24 hours in a WordPress transient (ott_supported_langs).

OTT_Language_Router resolves the effective locale for each request in priority order:

  1. Cookie (ott_user_lang) — validated against the supported-languages list; invalid codes are ignored and cleared.
  2. Accept-Language header — only evaluated when ltp_detect_browser_locale is true; the highest-quality browser locale that matches a supported LibreTranslate language wins.
  3. ltp_target_lang option — global fallback; always present.

Validation mode (ltp_validation_mode): when enabled, translation is applied only for manage_options users. All other visitors receive the original untranslated site — ideal for previewing translations before making them public.

The ott_user_lang cookie is written with HttpOnly => true, SameSite => Lax, scoped to the WordPress site path. It is never readable by front-end JavaScript.


Front-end language switcher

Shortcode: [open_tongue_switcher] — renders a <select> dropdown. Add style="list" for a <ul class="ott-lang-list"> link list.

Block: ott/language-switcher — available in the Gutenberg block inserter under the Open Tongue category; renders via the same shortcode output.

When a visitor selects a language, a fetch() call posts to POST /wp-json/ott/v1/set-lang, the cookie is updated, and the page reloads. All future requests from that browser use the cookie value, bypassing header detection.


Connection modes

Mode Description
localhost HTTP to 127.0.0.1:5000 via WP_Http. Default.
socket Raw cURL over a Unix domain socket (zero TCP overhead).
vpc HTTP to a private RFC 1918 IP with optional Bearer token auth.

The active mode is selected by the ltp_connection_mode WordPress option (localhost | socket | vpc).


WP-CLI command suite

Command Key options Description
wp ott translate batch <locale> --force, --dry-run, --post-type, --format Bulk-translate all posts
wp ott translate post <ID> <locale> Translate one post, purge static cache
wp ott translate string <text> <locale> Spot-test; never persists
wp ott cache warm --locale Pre-populate object cache from DB
wp ott cache flush --locale, --yes Flush one or all locales
wp ott cache status --format Show counts, backend, last prune
wp ott glossary import <file> --overwrite Stream CSV into DB
wp ott glossary export <file> --protected-only Stream DB to CSV
wp ott glossary list --format Tabular glossary listing
wp ott status --verbose Health check; exits 1 on failure

HTML-aware translation pipeline

Before text reaches the LibreTranslate API it passes through a three-stage protection pipeline:

  1. AttributePreserver — replaces href, src, srcset, action, formaction, poster, cite, and data-* attribute values with [[OTT_ATTR_n]] tokens so URL-like values are never mangled.
  2. TagProtector — replaces every HTML tag with [[OTT_TAG_n]] tokens. Prefers DOMDocument when dom and mbstring are available; falls back to an attribute-aware regex otherwise.
  3. HtmlAwareTranslator — orchestrates the 9-step pipeline: shouldExclude → maskExcluded → attrPreserver.protect → tagProtector.protect → ott_pre_translate → API call → tagProtector.restore → attrPreserver.restore → unmaskExcluded. Falls back to the original text if tag counts do not match after restore.

Exclusion rules engine

Rule type Example value How it works
css_selector .no-translate Converted to XPath internally; matched nodes are masked before the API call
regex /\bACME Corp\b/ Applied as a text-node fast-path check via preg_match()
xpath //code\|//pre Evaluated directly on the DOMDocument tree

Rules are stored in {prefix}ott_exclusion_rules and can be scoped to global, a specific post_type, or an individual post_id. The ott_exclusion_rules filter lets developers inject programmatic rules without a database entry.


Privacy

This plugin makes no outbound connections to any third-party service. All translation traffic is routed exclusively to the LibreTranslate endpoint you configure, which must resolve to a loopback address, a Unix socket, or an RFC 1918 private IP. The VPC driver actively refuses requests to any public IP address and logs the attempt.


Installation

  1. Upload the open-tongue-translations folder to /wp-content/plugins/.
  2. Activate the plugin through the Plugins screen in WordPress.
  3. Ensure a LibreTranslate instance is running and reachable from the WordPress host.
  4. Configure the plugin via the Open Tongue settings page in the WordPress admin, or use WP-CLI:

Localhost mode (default)

wp option update ltp_connection_mode localhost
wp option update ltp_localhost_host   127.0.0.1
wp option update ltp_localhost_port   5000
wp option update ltp_target_lang      fr

Unix socket mode

wp option update ltp_connection_mode socket
wp option update ltp_socket_path      /run/libretranslate/libretranslate.sock
wp option update ltp_target_lang      de

Private VPC mode

wp option update ltp_connection_mode vpc
wp option update ltp_vpc_ip           10.0.1.50
wp option update ltp_vpc_port         5000
wp option update ltp_vpc_api_key      your-secret-key   # optional
wp option update ltp_target_lang      es

Optional tuning

# Days before unused translations are pruned (default: 90)
wp option update ltp_prune_days 60

# Enable browser locale auto-detection
wp option update ltp_detect_browser_locale 1

# Enable validation mode (admin-only preview)
wp option update ltp_validation_mode 1

# Cloudflare direct-API credentials (Mode B — skip if using the CF plugin)
wp option update ltp_cf_zone_id    your-zone-id
wp option update ltp_cf_api_token  your-api-token

Read the full README on GitHub →