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
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.zipReadme
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:
- Gettext Interception — hooks into WordPress's own translation pipeline (
gettextfilter) to translate strings that pass through__(),_e(), and related functions. - 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 singleMGETround-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 orrocket_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:
- Cookie (
ott_user_lang) — validated against the supported-languages list; invalid codes are ignored and cleared. Accept-Languageheader — only evaluated whenltp_detect_browser_localeistrue; the highest-quality browser locale that matches a supported LibreTranslate language wins.ltp_target_langoption — 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:
- AttributePreserver — replaces
href,src,srcset,action,formaction,poster,cite, anddata-*attribute values with[[OTT_ATTR_n]]tokens so URL-like values are never mangled. - TagProtector — replaces every HTML tag with
[[OTT_TAG_n]]tokens. PrefersDOMDocumentwhendomandmbstringare available; falls back to an attribute-aware regex otherwise. - 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
- Upload the
open-tongue-translationsfolder to/wp-content/plugins/. - Activate the plugin through the Plugins screen in WordPress.
- Ensure a LibreTranslate instance is running and reachable from the WordPress host.
- 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