LF_AI_Client is the in-process façade for AI translation. Every plugin path that needs the AI (Visual Editor “AI Translate All”, per-segment AI re-generate, Bulk Actions, Frontend Visual Editor, comment translation, SEO meta translation, and the SEO Forge bridge) ultimately funnels through LF_AI_Client::translate() or translate_bulk() so the same enforcement rules apply everywhere.
Free/PRO split gates
The Free plugin must not infer PRO from stale Freemius or Forge Suite state. LF_License::effective_plan() first requires the LF_Pro_Loader class, which only exists when langforge-pro is active. Without that loader, the plan is always free even if fs_accounts, langforge_license_data, lf_license_data, or forge_license_last_pro_at still contain old values.
In local/dev test environments where FORGE_SKIP_LICENSE_REVALIDATION is true and the Pro add-on is active, forge_e2e_plan=pro|trial can still force a plan. Without the Pro loader, even a fresh forge_e2e_plan or forge_license_last_pro_at value must resolve to Free. If the override is absent and forge_license_last_pro_at is present on a Pro-capable install, the timestamp is only a transient revalidation hint; expired/cancelled registered licenses and Free-only installs force Free.
Every AI entry point should check LF_License::can('ai_translation') before instantiating LF_AI_Client. Current guarded handlers include String Translation bulk AI, Visual Editor AI translate, Frontend Editor AI translate, and queued bulk translation jobs. This keeps DevTools-enabled buttons, cron/queue workers, and crafted AJAX requests from bypassing the UI lock.
Runtime reads use the effective language list from lf_active_languages. Free and PRO both allow an unlimited number of active languages, so the full saved list participates in detection, routing, and switcher output regardless of license state — there is no language cap to enforce on downgrade.
Credits pricing proxy
The shared credits widget uses the same-origin AJAX action forge_credits_pricing to refresh runtime action costs. The browser sends action=forge_credits_pricing, product slug, and a forge_credits_pricing nonce to admin-ajax.php; WordPress then calls GET /credits/pricing server-side and returns the actions table (and pack list when present).
Do not reintroduce direct browser fetches to https://api.avakode.com/credits/pricing from plugin admin pages. Same-origin proxying avoids localhost CORS drift and keeps the Worker as the pricing source of truth without requiring development CORS flags.
PRO add-on update channel
langforge-pro registers Forge_Suite_Updater against the base product slug langforge, not langforge-pro. The Worker distributes PRO packages from /updates/langforge?tier=pro... and returns token-gated packages under /releases/langforge/pro/{version}. Using the add-on slug in the updater returns an unknown-plugin response and prevents wp-admin from seeing new PRO builds.
Method signatures
LF_AI_Client::instance()->translate(
string $text,
string $source_lang,
string $target_lang,
string $context = ''
): string|WP_Error;
LF_AI_Client::instance()->translate_bulk(
array $segments, // [['key' => '...', 'text' => '...'], …]
string $source_lang,
string $target_lang
): array|WP_Error;Glossary forwarding (audit-r3 R3-G1)
Both methods automatically fetch the active Glossary (a Free feature) for the $source_lang → $target_lang pair and attach it to the request payload sent to forge-api /ai/process. The Glossary itself is available on every plan; this forwarding path only runs through the PRO AI client, so the glossary is applied automatically only when AI translation (PRO) is in use. The mapping plugin → worker schema lives in LF_Glossary::get_payload_for_translate():
| Glossary row | Worker payload entry | Effect on AI prompt |
|---|---|---|
do_not_translate = 1 | {"source": "Term", "target": "Term"} | AI keeps the term verbatim |
do_not_translate = 0, custom target | {"source": "Term", "target": "Custom RU"} | AI uses the user’s preferred translation |
do_not_translate = 0, empty target | (skipped) | nothing actionable to enforce |
Single-segment calls (translate()) only forward terms that actually appear in the source text — checked via the same word-boundary regex used by the source-column highlighter, with the row’s case_sensitive flag honoured. Bulk calls (translate_bulk()) forward every term for the lang pair, since per-segment filtering would mean re-querying the database for every short string.
If the active glossary has no entries for the pair, the payload is empty [] — no behaviour change. (Since the Glossary is now a Free feature, LF_License::can('glossary') returns true on all plans; the AI forwarding above still only fires when the PRO AI translate path runs.)
Translation Memory auto-populate (audit-r3 R3-T1)
After every successful single-segment translate, LF_AI_Client::translate() writes the source/translated pair to wp_lf_translation_memory via LF_Translation_Memory::add(). translate_bulk() does the same for each segment. Dedupe + noise filtering rules:
- Source < 3 chars or contains no letters → silently dropped.
source == target(case-folded) → silently dropped.- Same
source_hash + source_lang + target_langalready exists →usage_countbumped,translated_textoverwritten with the latest translation.
This makes single-segment AI calls (per-field “AI Translate” in the Visual Editor, comment translation, SEO meta refresh) populate TM the same way full-post bulk translations do via the capture_post_translation save_post hook. Before this fix only the post-level path wrote to TM.
Programmatic glossary API
If you build a custom workflow that calls the worker directly (bypassing LF_AI_Client), you can fetch the same payload via the public glossary helper:
$payload = LF_Glossary::instance()
->get_payload_for_translate('en', 'ru', 'optional source text');
// $payload === [['source' => 'Term', 'target' => 'Term'], …]Pass the returned array as the 5th argument to LF_Forge_API::ai_translate():
LF_Forge_API::instance()->ai_translate(
'Hello world',
'en', 'ru',
'context-string',
$payload // the glossary
);LF_Forge_API::ai_translate_parallel() accepts the glossary as its 6th argument and forwards it to every chunked translate request inside the curl_multi batch.
—