WordPress multilingual sites development often brings a lot of challenges during development. We know that creating and maintaining multilingual WordPress and WooCommerce sites can become overwhelming over time. That's why we focus on building performance focused and maintainable solutions for our clients thanks to Polylang and WS Form.
There is just one issue with WS Form. It doesn't natively support multilingual plugins like Polylang or WPML. The common workaround? Duplicate the entire form for each language which is the official recommendation from WS Form team. This can quickly becomes a maintenance nightmare – every change requires updating multiple forms.
Main problem of duplicating forms
When you duplicate forms for translations, you end up with:
- Multiple forms to maintain
- Inconsistent field IDs across languages (breaking integrations)
- Risk of forgetting to sync changes
There's a better way: hook into WS Form's rendering process and translate field properties on the fly.
The Solution
Use the wsf_pre_render_{form_id} filter to modify field labels, placeholders, help text, and validation messages before the form renders. This filter allows you to modify whole form object before it renders. Which is very useful for translation. One form, multiple languages.
The Translation Function
Main concept of translating WS Form's form using PHP without duplicating it is to define one reusable function, which will allow us to just define config array of form strings and pass them to it.
/**
* Update WS Form field properties (label, placeholder, help, invalid_feedback).
* Generates default invalid_feedback when none is provided.
*
* @param stdClass $form_object The WS Form object.
* @param array $fields_config Field configurations keyed by field ID.
* @return stdClass Modified form object.
*/
function webbaker_wsf_update_fields( $form_object, $fields_config = [] ) {
if ( empty( $form_object->groups ) ) {
return $form_object;
}
// Default validation message – customize for your text domain
$default_label = _x( 'This field', 'form validation', 'text-domain' );
$default_suffix = _x( 'is required', 'form validation', 'text-domain' );
foreach ( $form_object->groups as $group ) {
if ( empty( $group->sections ) ) {
continue;
}
foreach ( $group->sections as $section ) {
if ( empty( $section->fields ) ) {
continue;
}
foreach ( $section->fields as $field ) {
$field_id = isset( $field->id ) ? (int) $field->id : 0;
// Apply custom translations for this field
if ( isset( $fields_config[ $field_id ] ) ) {
$changes = $fields_config[ $field_id ];
if ( isset( $changes['label'] ) ) {
$field->label = $changes['label'];
}
if ( isset( $changes['placeholder'] ) && isset( $field->meta ) ) {
$field->meta->placeholder = $changes['placeholder'];
}
if ( isset( $changes['help'] ) && isset( $field->meta ) ) {
$field->meta->help = $changes['help'];
}
if ( isset( $changes['invalid_feedback'] ) && isset( $field->meta ) ) {
$field->meta->invalid_feedback = $changes['invalid_feedback'];
}
}
// Auto-generate validation message if empty
if ( isset( $field->meta ) && empty( $field->meta->invalid_feedback ) ) {
$field->meta->invalid_feedback = sprintf(
'%s %s',
$default_label,
$default_suffix
);
}
}
}
}
return $form_object;
}Translate WS Form - Simple Contact form
Let's say we have simple contact form with ID: 1 which we want to translate to other language. It consists of basic fields like Full Name, Email, Message and Submit button. Keep in mind that we left all placeholder and help texts empty for every field.

After inserting form into page, this is what it should look like:

Translating WS Form using Code Snippets
You could create separate plugin for this translation integration or just use your favorite plugin for inserting code snippets. In this tutorial we can show how easy it is to translate WS Form using Code Snippets plugin and also include Polylang integration with Strings Translation.
In Code Snippets plugin we should create two snippets. First one would represent main reusable function shown earlier in this article which we will reuse in all translation scripts later. Feel free to just copy and paste it. In this example in screenshot below, snippet is called WS Form - Reusable translation Script.

After that, let's create script specific for translating our Contact form. We will create second script, in this example called WS Form - Translate contact form fields. We will define array of fields strings which needs to be translated.
Hook into your specific form using wsf_pre_render_{form_id}. In our case, form ID is 1 so hook should be wsf_pre_render_1.
Field IDs can be found in WS Form's form builder – each field shows its ID in the sidebar when selected. In most cases we integrate this method directly in custom themes, which are often build as whole product or solution to customer.
Let's add "Prefix - " to all strings so we can be sure they are coming out of our snippet instead of default WS Form config.
add_filter( 'wsf_pre_render_1', function ( $form_object, $preview ) {
$fields = [
3 => [
'label' => __( 'Prefix - Full Name', 'text-domain' ),
'placeholder' => __( 'Enter your name', 'text-domain' ),
],
5 => [
'label' => __( 'Prefix - Your email', 'text-domain' ),
'placeholder' => __( 'you@example.com', 'text-domain' ),
],
6 => [
'label' => __( 'Prefix - Your Message', 'text-domain' ),
'placeholder' => __( 'How can we help?', 'text-domain' ),
],
4 => [
'label' => __( 'Prefix - Submit', 'text-domain' ),
],
];
return webbaker_wsf_update_fields( $form_object, $fields );
}, 10, 2 );After saving snippet and looking at the rendered form on frontend, you should see exactly this. Do you remember that we left all placeholders empty when creating form in WS Form edit screen? Now you can see that we just created placeholder during form rendering which can be also translatable. Basically you can manipulate whole WS Form object before rendering.

Polylang Strings Translation integration
Let's add Polylang Strings translation to our setup and also extend array of fields config with help and invalid feedback strings.
To keep things organized, lets edit already existing snippet created for translating contact form.
add_action( 'plugins_loaded', function () {
if ( ! function_exists( 'pll_register_string' ) ) {
return;
}
pll_register_string( 'contact-name-label', 'Full Name', 'Contact Form' );
pll_register_string( 'contact-name-placeholder', 'Enter your name', 'Contact Form' );
pll_register_string( 'contact-name-help', 'Please provide your full name.', 'Contact Form' );
pll_register_string( 'contact-email-label', 'Your email', 'Contact Form' );
pll_register_string( 'contact-email-placeholder', 'you@example.com', 'Contact Form' );
pll_register_string( 'contact-email-invalid-feedback', 'Please provide a valid email address.', 'Contact Form' );
pll_register_string( 'contact-message-label', 'Your Message', 'Contact Form' );
pll_register_string( 'contact-message-placeholder', 'How can we help?', 'Contact Form' );
pll_register_string( 'contact-message-help', 'Feel free to ask us anything.', 'Contact Form' );
pll_register_string( 'contact-submit-label', 'Submit', 'Contact Form' );
} );
add_filter( 'wsf_pre_render_1', function ( $form_object, $preview ) {
if ( ! function_exists( 'pll__' ) ) {
return $form_object;
}
$fields = [
3 => [
'label' => pll__( 'Full Name' ),
'placeholder' => pll__( 'Enter your name' ),
'help' => pll__( 'Please provide your full name.' ),
],
5 => [
'label' => pll__( 'Your email' ),
'placeholder' => pll__( 'you@example.com' ),
'invalid_feedback' => pll__( 'Please provide a valid email address.' ),
],
6 => [
'label' => pll__( 'Your Message' ),
'placeholder' => pll__( 'How can we help?' ),
'help' => pll__( 'Feel free to ask us anything.' ),
],
4 => [
'label' => pll__( 'Submit' ),
],
];
return webbaker_wsf_update_fields( $form_object, $fields );
}, 10, 2 );After saving edited script, you should see strings properly registered in Polylang's String translation interface. Let's try to edit and translate them.

That's it. Now you have single WS Form translated into multiple languages using just Polylang Strings translations.
Our workflow
Primary focus of this tutorial is to demonstrate how translating WS Form can be done. In our workflow we usually export all strings using standardized way with wp i18n make-pot, which are then translated using Poedit and its DeepL integration. This brings almost full automation to translating forms and keeps everything in sync with the rest of the theme translations.
Summary
This approach gives you:
- Single source of truth – one form to maintain across all languages
- Consistent field IDs – integrations, analytics, and conditional logic work reliably
- Standard WordPress i18n – translations live in Polylang, Loco Translate, or PO/MO files
- Clean separation – form structure stays in WS Form, translations live in code
The function handles labels, placeholders, help text, and validation messages. For checkbox, radio, and select field options, WS Form offers a data source hook approach which we'll cover in a future article.
Limitations
We are completely aware that this workaround will not cover all use-cases of using WS Form translations. It requires identifying field IDs manually and works best when you control the theme or have access to code snippets. For sites with dozens of forms managed by non-technical editors, duplicating forms might still be the pragmatic choice.
However, this method is proven to be more than usable on lot of our client's websites. Hope this helps you to create more maintainable and scalable WordPress websites!