Skip to content

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.


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.


Custom tabs appear in the Omnalingo settings navigation alongside General, Languages, Glossary, AI Context, Exclusion Rules, Advanced, and License.

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);

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
);
});

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 directly

If 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
});

Groups inject a section inside an existing core tab, without adding a new top-level tab.

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 IDCore groups (order)
generallanguages (10), content (20)
glossaryglossary (10)
ai-contextai-context (10), translation-style (20)
exclusion-rulesexclusions (10)
advancedscanning (10), database (20)

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);

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.

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.

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 a successful save, Omnalingo fires omnalingo:ext-saved and resets data-dirty to "false":

mountEl.addEventListener('omnalingo:ext-saved', () => {
savedSnapshot = { ...currentFormState };
});

Extension settings are stored under omnalingo_options.extensions.{extension_id} in the WordPress options table.

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.

Extension settings are included in the hydration object at page load:

const mySettings = window.omnalingoData.omnalingo_options?.extensions?.['my-feature'] || {};

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.


EventTarget elementFired whene.detail
omnalingo:ext-mountedYour mount <div>Tab or group first rendersnone
omnalingo:ext-activatedYour mount <div>Tab re-activated after Vue keep-alivenone
omnalingo:ext-saveYour mount <div>User clicks Save All Changes{ resolve, reject }
omnalingo:ext-savedYour mount <div>Save completed successfullynone

All events bubble. When attaching to document rather than the mount element directly, use the capture phase (true as the third argument to addEventListener).


FilterArgumentsDefaultPurpose
omnalingo/containernullRead-only container wrapper
omnalingo/settings/tabsarray $tabs[]Register new settings tabs
omnalingo/settings/groupsarray $groups[]Register groups within tabs
omnalingo/settings/max_languagesint $max1Override the active language limit
omnalingo/admin/hydration_dataarray $dataFull hydration payloadAdd data to window.omnalingoData
omnalingo_validate_extension_{id}array $datapassthroughSanitize extension settings on save
omnalingo/db/table_prefixstring $prefix$wpdb->prefixOverride all table name prefixes
omnalingo/slug/translate_urlstring $url, string $langCode, ?int $postId, ?int $termId$urlApply per-slug translations to generated URLs
omnalingo/slug/resolve_pathstring $path, string $langCode$pathReverse-map translated slugs during routing
omnalingo/slug/translationsarray $translations, int $objectId, string $objectType[]Return stored slug translations for the editor
omnalingo/scan/translatable_meta_tagsarray $allowedMetaTagsDefault listOverride which <meta> tag names are scanned
omnalingo/slug/batch_sizeint $size200Set slug batch size for editor operations