Fillin 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 Fillin 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. "Name")
// $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, after its row in wp_formforge_forms and its submissions and analytics rows have been removed. 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 a translation plugin can read the language from the submit request (Fillin Forge itself does not read this field; 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.
The language during the AJAX submit
Fillin Forge puts the visitor’s language into the hidden formforge_lang field of the form (see formforge_current_language above). The field comes back in the submit request, and Fillin Forge builds the success message through formforge_translate_string, which receives only $string, $key, $field_id and $form_id. Fillin Forge does not read formforge_lang itself and has no filter to override the language at that point. A translation plugin that wants the language has to read $_POST['formforge_lang'] itself, inside its formforge_translate_string listener. Lang Forge does this in a filter of its own, langforge_formforge_translate_lang_override, which belongs to Lang Forge and not to Fillin Forge.
Per-form string domain
The Lang Forge listener uses formforge: as the per-form string_domain in wp_langforge_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, Fillin 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 Fillin Forge one).
—