Extending Omnalingo
Omnalingo provides several extension points for companion plugins: a read-only service container, PHP filters to add UI into the settings screens, JavaScript CustomEvents to coordinate save operations, and lifecycle actions to react to gathering, scanning, and translation events.
Accessing Core Services
Section titled “Accessing Core Services”To access a core service from a companion plugin, use the omnalingo/container filter:
add_action('init', function () { $container = apply_filters('omnalingo/container', null); if ($container && $container->has(\Omnalingo\Service\SlugService::class)) { $slugService = $container->get(\Omnalingo\Service\SlugService::class); }});The container filter returns a read-only wrapper — you can call get() and has(), but not set(). Extensions cannot replace registered services.
Hook into this on init or later. The container is populated during plugins_loaded and the registration order between plugins is not guaranteed.
Adding a Settings Tab
Section titled “Adding a Settings Tab”Custom tabs appear in the Omnalingo settings navigation alongside General, Languages, Glossary, AI Context, Exclusion Rules, Advanced, and License.
1. Register the tab (PHP)
Section titled “1. Register the tab (PHP)”add_filter('omnalingo/settings/tabs', function (array $tabs): array { $tabs[] = [ 'slug' => 'my-feature', // becomes the Vue route: #/settings/my-feature 'label' => __('My Feature', 'my-plugin'), 'order' => 35, // General=10, Languages=15, Glossary=20, AI Context=25, Exclusion Rules=30, Advanced=40, License=50 ]; return $tabs;}, 10, 1);2. Enqueue your script (PHP)
Section titled “2. Enqueue your script (PHP)”Your script must load after the main Omnalingo app so the mount point element is in the DOM when your code runs:
add_action('admin_enqueue_scripts', function (string $hook): void { if (strpos($hook, 'omnalingo') === false) { return; } wp_enqueue_script( 'my-plugin-admin', MY_PLUGIN_URL . 'assets/admin.js', ['jquery'], // add the Omnalingo app handle as a dependency if you need it MY_PLUGIN_VERSION, true );});3. Mount your UI (JavaScript)
Section titled “3. Mount your UI (JavaScript)”When the Vue app renders your tab for the first time, it creates a <div id="omnalingo-ext-my-feature"> and fires a omnalingo:ext-mounted CustomEvent on it:
document.addEventListener('omnalingo:ext-mounted', (e) => { if (e.target.id !== 'omnalingo-ext-my-feature') return; e.target.innerHTML = '<p>My extension content</p>';}, true); // capture phase required — event is dispatched on the div directlyIf you use Vue for your extension UI:
import { createApp } from 'vue';import MyComponent from './MyComponent.vue';
document.addEventListener('omnalingo:ext-mounted', (e) => { if (e.target.id !== 'omnalingo-ext-my-feature') return; createApp(MyComponent).mount(e.target);}, true);When the user navigates away from your tab and back, the event is omnalingo:ext-activated (not ext-mounted). Use this to refresh data without remounting:
document.getElementById('omnalingo-ext-my-feature') ?.addEventListener('omnalingo:ext-activated', () => { // Refresh data as needed });Adding a Settings Group
Section titled “Adding a Settings Group”Groups inject a section inside an existing core tab, without adding a new top-level tab.
Register the group (PHP)
Section titled “Register the group (PHP)”add_filter('omnalingo/settings/groups', function (array $groups): array { $groups['general'][] = [ 'id' => 'my-seo', 'order' => 15, // after "languages" (10), before "content" (20) 'type' => 'extension', ]; return $groups;}, 10, 1);Available tab IDs and their existing groups:
| Tab ID | Core groups (order) |
|---|---|
general | languages (10), content (20) |
glossary | glossary (10) |
ai-context | ai-context (10), translation-style (20) |
exclusion-rules | exclusions (10) |
advanced | scanning (10), database (20) |
Mount your group UI (JavaScript)
Section titled “Mount your group UI (JavaScript)”Group mount divs use the ID pattern omnalingo-group-{tabId}-{groupId}:
document.addEventListener('omnalingo:ext-mounted', (e) => { if (e.target.id !== 'omnalingo-group-general-my-seo') return; e.target.innerHTML = '<div>My SEO settings section</div>';}, true);Save Coordination
Section titled “Save Coordination”When the user clicks Save All Changes, Omnalingo fires omnalingo:ext-save on each extension mount element. Your extension must call resolve or reject from e.detail to signal completion. The main save waits for all extensions before finishing.
Signaling unsaved changes
Section titled “Signaling unsaved changes”Set data-dirty="true" on your mount element whenever the user has made changes:
const mountEl = document.getElementById('omnalingo-ext-my-feature');// Whenever a form value changes:mountEl.dataset.dirty = 'true';Omnalingo reads this attribute to decide whether to include your extension in the save batch. Elements with data-dirty="false" are skipped.
Tab extensions: save to your own endpoint
Section titled “Tab extensions: save to your own endpoint”mountEl.addEventListener('omnalingo:ext-save', async (e) => { const { resolve, reject } = e.detail; try { await wp.apiFetch({ path: '/omnalingo/v1/settings', method: 'POST', data: { extensions: { 'my-feature': { enabled: true } } } }); resolve(); } catch (err) { reject(err); }});Group extensions: return data for the orchestrator
Section titled “Group extensions: return data for the orchestrator”mountEl.addEventListener('omnalingo:ext-save', (e) => { const { resolve } = e.detail; resolve({ 'my-feature': { enabled: true, option_a: 'value' } });});Group data is merged into the main settings payload and sent in a single request.
After save
Section titled “After save”After a successful save, Omnalingo fires omnalingo:ext-saved and resets data-dirty to "false":
mountEl.addEventListener('omnalingo:ext-saved', () => { savedSnapshot = { ...currentFormState };});Extension Settings Storage
Section titled “Extension Settings Storage”Extension settings are stored under omnalingo_options.extensions.{extension_id} in the WordPress options table.
Validating settings on save
Section titled “Validating settings on save”Register a filter to sanitize your extension’s data before it is written:
add_filter('omnalingo_validate_extension_my-feature', function (array $data): array { return [ 'enabled' => !empty($data['enabled']), 'option_a' => sanitize_text_field($data['option_a'] ?? ''), ];}, 10, 1);The filter name is omnalingo_validate_extension_{id} where {id} matches the slug from your tab or the id from your group.
Reading settings in JavaScript
Section titled “Reading settings in JavaScript”Extension settings are included in the hydration object at page load:
const mySettings = window.omnalingoData.omnalingo_options?.extensions?.['my-feature'] || {};Adding data to the hydration object
Section titled “Adding data to the hydration object”Use omnalingo/admin/hydration_data to inject anything else your JavaScript needs:
add_filter('omnalingo/admin/hydration_data', function (array $data): array { $data['myPluginConfig'] = [ 'apiUrl' => MY_PLUGIN_API_URL, 'options' => get_option('my_plugin_options', []), ]; return $data;}, 10, 1);Accessible in JavaScript as window.omnalingoData.myPluginConfig.
CustomEvent Reference
Section titled “CustomEvent Reference”| Event | Target element | Fired when | e.detail |
|---|---|---|---|
omnalingo:ext-mounted | Your mount <div> | Tab or group first renders | none |
omnalingo:ext-activated | Your mount <div> | Tab re-activated after Vue keep-alive | none |
omnalingo:ext-save | Your mount <div> | User clicks Save All Changes | { resolve, reject } |
omnalingo:ext-saved | Your mount <div> | Save completed successfully | none |
All events bubble. When attaching to document rather than the mount element directly, use the capture phase (true as the third argument to addEventListener).
PHP Filter Reference
Section titled “PHP Filter Reference”| Filter | Arguments | Default | Purpose |
|---|---|---|---|
omnalingo/container | null | — | Read-only container wrapper |
omnalingo/settings/tabs | array $tabs | [] | Register new settings tabs |
omnalingo/settings/groups | array $groups | [] | Register groups within tabs |
omnalingo/settings/max_languages | int $max | 1 | Override the active language limit |
omnalingo/admin/hydration_data | array $data | Full hydration payload | Add data to window.omnalingoData |
omnalingo_validate_extension_{id} | array $data | passthrough | Sanitize extension settings on save |
omnalingo/db/table_prefix | string $prefix | $wpdb->prefix | Override all table name prefixes |
omnalingo/slug/translate_url | string $url, string $langCode, ?int $postId, ?int $termId | $url | Apply per-slug translations to generated URLs |
omnalingo/slug/resolve_path | string $path, string $langCode | $path | Reverse-map translated slugs during routing |
omnalingo/slug/translations | array $translations, int $objectId, string $objectType | [] | Return stored slug translations for the editor |
omnalingo/scan/translatable_meta_tags | array $allowedMetaTags | Default list | Override which <meta> tag names are scanned |
omnalingo/slug/batch_size | int $size | 200 | Set slug batch size for editor operations |