Form Forge exposes a small set of hooks so a multilingual stack (Lang Forge first-party, anything compatible by contract) can localize labels, placeholders, options, and form-level text. The hooks are passive — without a listener the original strings are emitted unchanged, so sites without a translation plugin keep working exactly the way they did before.
Filter: formforge_translate_string
Fires for every translatable string Form Forge renders (labels, placeholders, descriptions, option labels, submit button, success message, schedule message). A translation plugin attaches to this filter and returns the translated string for the current language.
add_filter('formforge_translate_string', function ($string, $key, $field_id, $form_id) {
// $string - the source-language string (e.g. "Имя")
// $key - which slot the string fills:
// 'label', 'placeholder', 'description',
// 'option_label:<value>',
// 'submit_text', 'success_message', 'schedule_message'
// $field_id - the field's `id` from wp_formforge_forms.fields. Empty
// string for form-level keys.
// $form_id - integer form id.
if ($key === 'label' && $field_id === 'name') {
// Look up your translation here.
return my_plugin_translate($string, "form-{$form_id}-name");
}
return $string;
}, 10, 4);Returning the original $string is the correct fallback when no translation is available — the renderer escapes the result, so the user never sees a missing-translation marker.
Action: formforge/strings_registered
Fires after a form is created or updated, with every translatable string in a single call. Listeners use this to register strings in their translation tables in batches.
add_action('formforge/strings_registered', function ($form_id, $strings) {
// $strings is an array of:
// ['key' => '<key>', 'field_id' => '<field_id>', 'value' => '<source string>']
foreach ($strings as $entry) {
my_plugin_register_string($form_id, $entry['key'], $entry['field_id'], $entry['value']);
}
}, 10, 2);Re-fires on every save, so renamed labels and added options end up in your translation table without an explicit re-import. Listeners should treat registration as idempotent (INSERT IGNORE / upsert by (form_id, key, field_id)).
Action: formforge/strings_unregistered
Fires when a form is deleted, before the row is removed from wp_formforge_forms. Use this to clean up translations attached to the form.
add_action('formforge/strings_unregistered', function ($form_id) {
my_plugin_delete_form_strings($form_id);
}, 10, 1);Filter: formforge_current_language
Returns the visitor’s current language at form-render time. The result is emitted into the rendered form as a hidden formforge_lang input so the AJAX submit handler can resolve translations correctly (admin-ajax requests don’t carry the URL-prefix or ?lang= signal a multilingual plugin would normally use to detect the visitor’s language).
add_filter('formforge_current_language', function ($default, $form_id) {
return my_plugin_get_current_language() ?: $default;
}, 10, 2);Return '' (the default) to skip the hidden field entirely.
Filter: formforge_translate_lang_override
Fires inside formforge_translate_string listeners (when used together) with the visitor’s language as resolved from the submit payload. Translation plugins use this to override the runtime “current language” lookup during AJAX submit handling so success messages come back in the visitor’s language, not the site default.
add_filter('formforge_translate_lang_override', function ($lang, $form_id) {
if (!empty($_POST['formforge_lang'])) {
return sanitize_text_field(wp_unslash($_POST['formforge_lang']));
}
return $lang;
}, 10, 2);Per-form string domain
The Lang Forge listener uses formforge: as the per-form string_domain in wp_lf_strings. This keeps every form’s strings grouped together and makes per-form cleanup a single DELETE WHERE string_domain = ? call. If you are writing a non-LangForge listener, follow the same convention so admin tools that aggregate across translation backends can match strings to forms reliably.
Stable string names
To keep translations stable across re-saves, Form Forge does not include the source-language text in its generated string names. The names are:
- Field-level:
field__— e.g.field_name_label,field_email_placeholder,field_category_option_label:business - Form-level:
— e.g.submit_text,success_message,schedule_message
Renaming a field’s id will reset its translation key (intentional, because the identity of the field changed). Renaming a label’s text without changing the id keeps the existing translation row in place, so the translator’s previous work is preserved (you may want to mark it for review — that’s a translation-plugin concern, not a Form Forge one).
—