PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.8
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.8
2.12.8 2.12.7 2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 All 98 releases
sureforms / inc / compatibility / multilingual / string-translator.php

string-translator.php in SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz 2.12.8, at inc/compatibility/multilingual/string-translator.php

683 lines 25.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Multilingual String Translator.
4 *
5 * Provides convenient, name-scheme-aware helpers for translating SureForms'
6 * form-level metadata strings through the active multilingual provider.
7 *
8 * @package sureforms.
9 * @since 2.11.0
10 */
11
12 namespace SRFM\Inc\Compatibility\Multilingual;
13
14 use SRFM\Inc\Traits\Get_Instance;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit; // Exit if accessed directly.
18 }
19
20 /**
21 * String_Translator.
22 *
23 * Bridges SureForms render paths to the multilingual provider using a stable naming
24 * scheme that matches the strings registered on save by the string-collector.
25 *
26 * Per-form strings are translated through a WPML **String Package** (one package
27 * per form, see {@see form_package()}), so they appear grouped under the form in the
28 * Translation Editor instead of as flat global strings. Package-scoped string names
29 * (no `form_{id}_` prefix — the package is the form):
30 * - Submit button text: `submit_button`
31 * - Confirmation message: `confirmation_{N}_message`
32 * - Notification field: `notification_{N}_{subject|body|from_name|reply_to}`
33 * - Form restriction message: `restriction_message`
34 * - Block string attribute: `block_{block_id}_{attribute}`
35 * - Block option label: `block_{block_id}_option_{N}_label`
36 *
37 * Built-in validation messages stay flat global strings (domain `sureforms`,
38 * `validation_{key}`) since they are not per-form. When the provider can't do
39 * packages, per-form strings fall back to the legacy flat name `form_{id}_{name}`.
40 *
41 * @since 2.11.0
42 */
43 class String_Translator {
44 use Get_Instance;
45
46 /**
47 * Translation domain used for every SureForms string.
48 *
49 * @since 2.11.0
50 */
51 public const DOMAIN = 'sureforms';
52
53 /**
54 * WPML "kind" label for SureForms form string packages.
55 *
56 * @since 2.11.0
57 */
58 public const PACKAGE_KIND = 'SureForms Form';
59
60 /**
61 * Map of SureForms block name → translatable scalar attribute keys.
62 *
63 * Keys correspond to the `name` field in each block's `block.json`. Values
64 * are the attribute keys whose `string` value is shown to the visitor and
65 * therefore needs to be registered with WPML and translated at render time.
66 *
67 * Blocks that store option lists (dropdown, multi-choice) declare ONLY their
68 * scalar attributes here; their `options[*].label` strings are walked
69 * separately by {@see translatable_option_blocks()}.
70 *
71 * @since 2.11.0
72 * @return array<string, array<int, string>>
73 */
74 public static function translatable_block_attributes(): array {
75 $map = [
76 'srfm/input' => [ 'label', 'placeholder', 'help', 'errorMsg', 'defaultValue', 'duplicateMsg' ],
77 'srfm/email' => [ 'label', 'placeholder', 'help', 'errorMsg', 'defaultValue', 'duplicateMsg', 'confirmLabel' ],
78 'srfm/phone' => [ 'label', 'placeholder', 'help', 'errorMsg', 'duplicateMsg' ],
79 'srfm/number' => [ 'label', 'placeholder', 'help', 'errorMsg', 'defaultValue', 'prefix', 'suffix' ],
80 'srfm/url' => [ 'label', 'placeholder', 'help', 'errorMsg', 'defaultValue' ],
81 'srfm/textarea' => [ 'label', 'placeholder', 'help', 'errorMsg', 'defaultValue' ],
82 'srfm/address' => [ 'label', 'help' ],
83 'srfm/dropdown' => [ 'label', 'placeholder', 'help', 'errorMsg' ],
84 'srfm/multi-choice' => [ 'label', 'help', 'errorMsg' ],
85 'srfm/checkbox' => [ 'label', 'help', 'errorMsg' ],
86 'srfm/gdpr' => [ 'label', 'help', 'errorMsg' ],
87 'srfm/inline-button' => [ 'buttonText' ],
88 'srfm/payment' => [ 'label', 'help', 'errorMsg', 'amountLabel', 'paymentDescription', 'oneTimeLabel', 'subscriptionLabel' ],
89 ];
90
91 /**
92 * Filters the map of block name → translatable scalar attribute keys.
93 *
94 * Both the string-collector (registration on save) and the string-translator
95 * (translation at render) read this method, so a single filter keeps the two
96 * sides in lockstep. Pro field blocks register their translatable attributes here.
97 *
98 * @since 2.11.0
99 * @param array<string, array<int, string>> $map Block name → attribute keys.
100 */
101 $filtered = apply_filters( 'srfm_translatable_block_attributes', $map );
102
103 return is_array( $filtered ) ? $filtered : $map;
104 }
105
106 /**
107 * Block names whose `options` array contains translatable `label` entries.
108 *
109 * @since 2.11.0
110 * @return array<int, string>
111 */
112 public static function translatable_option_blocks(): array {
113 $blocks = [ 'srfm/dropdown', 'srfm/multi-choice' ];
114
115 /**
116 * Filters the list of block names whose `options[*].label` entries are translatable.
117 *
118 * Read by both the string-collector (registration) and the string-translator
119 * (translation), so a single filter keeps the two sides in lockstep. Pro
120 * option-bearing blocks opt into option-label translation here.
121 *
122 * @since 2.11.0
123 * @param array<int, string> $blocks Block names with translatable option labels.
124 */
125 $filtered = apply_filters( 'srfm_translatable_option_blocks', $blocks );
126
127 return is_array( $filtered ) ? $filtered : $blocks;
128 }
129
130 /**
131 * Human-readable field-type label for a SureForms block, used to group a
132 * field's strings in the Translation Editor (e.g. the "Email #2" heading).
133 *
134 * Deliberately uses the block TYPE rather than the user-entered field label:
135 * WPML builds the Translation-Editor tree by splitting the string title on
136 * `/` (nesting) and `: ` (path vs. leaf), so a user who types either sequence
137 * into a field label would otherwise corrupt the grouping. Block types are
138 * plugin-controlled and safe.
139 *
140 * @since 2.11.0
141 * @param string $block_name Full block name, e.g. `srfm/email`.
142 * @return string Friendly type label, e.g. `Email`.
143 */
144 public static function block_type_label( string $block_name ): string {
145 $labels = [
146 'srfm/input' => __( 'Text', 'sureforms' ),
147 'srfm/email' => __( 'Email', 'sureforms' ),
148 'srfm/phone' => __( 'Phone', 'sureforms' ),
149 'srfm/number' => __( 'Number', 'sureforms' ),
150 'srfm/url' => __( 'URL', 'sureforms' ),
151 'srfm/textarea' => __( 'Textarea', 'sureforms' ),
152 'srfm/address' => __( 'Address', 'sureforms' ),
153 'srfm/dropdown' => __( 'Dropdown', 'sureforms' ),
154 'srfm/multi-choice' => __( 'Multiple Choice', 'sureforms' ),
155 'srfm/checkbox' => __( 'Checkbox', 'sureforms' ),
156 'srfm/gdpr' => __( 'GDPR', 'sureforms' ),
157 'srfm/inline-button' => __( 'Button', 'sureforms' ),
158 'srfm/payment' => __( 'Payment', 'sureforms' ),
159 ];
160
161 /**
162 * Filters the block-name → field-type label map used to group a field's
163 * strings in the Translation Editor. Pro field blocks add their labels here.
164 *
165 * @since 2.11.0
166 * @param array<string, string> $labels Block name → friendly type label.
167 */
168 $labels = apply_filters( 'srfm_block_type_labels', $labels );
169
170 if ( isset( $labels[ $block_name ] ) && is_string( $labels[ $block_name ] ) ) {
171 return $labels[ $block_name ];
172 }
173
174 // Derive from the block name: strip the namespace, turn dashes into spaces
175 // and title-case (e.g. `srfm/date-time-picker` → `Date Time Picker`).
176 $slug = false !== strpos( $block_name, '/' ) ? substr( $block_name, strpos( $block_name, '/' ) + 1 ) : $block_name;
177 $slug = str_replace( '-', ' ', $slug );
178 return '' !== $slug ? ucwords( $slug ) : __( 'Field', 'sureforms' );
179 }
180
181 /**
182 * Build the string-package descriptor for a form.
183 *
184 * One package per form (named by post ID) so every form string surfaces
185 * together under the form in the multilingual plugin's Translation Editor,
186 * instead of as flat, global String-Translation entries. The collector
187 * (registration) and the translator (render) both call this, keeping the two
188 * sides in lockstep.
189 *
190 * @param int $form_id Form post ID.
191 * @since 2.11.0
192 * @return array<string,string> { kind, name, title, edit_link, post_id }.
193 */
194 public static function form_package( int $form_id ): array {
195 $title = get_the_title( $form_id );
196 if ( '' === $title ) {
197 /* translators: %d is the form ID. */
198 $title = sprintf( __( 'SureForms Form #%d', 'sureforms' ), $form_id );
199 }
200
201 $edit_link = get_edit_post_link( $form_id, 'raw' );
202
203 return [
204 'kind' => self::PACKAGE_KIND,
205 'name' => (string) $form_id,
206 'title' => $title,
207 'edit_link' => is_string( $edit_link ) ? $edit_link : '',
208 // Associate the package with its form post so WPML's "Translate
209 // Everything Automatically" queues the package alongside the post.
210 // NOTE: pending live-WPML confirmation of the exact key WPML reads
211 // for post association — `cms_id` is the documented fallback if
212 // `post_id` isn't consumed. Tracked in issue #2942.
213 'post_id' => (string) $form_id,
214 ];
215 }
216
217 /**
218 * Package-scoped submit-button string name. Names are unique within a form's
219 * package (which is itself scoped to the form), so they carry no `form_{id}_`
220 * prefix; the legacy flat-string fallback re-adds it in {@see dispatch_package()}.
221 *
222 * @since 2.11.0
223 * @return string
224 */
225 public static function submit_button_name(): string {
226 return 'submit_button';
227 }
228
229 /**
230 * Package-scoped form-title string name. The form's post title is shown as a
231 * heading on the form (and as the instant-form banner), so it needs to be
232 * registered and translated like any other user-facing string.
233 *
234 * @since 2.12.3
235 * @return string
236 */
237 public static function title_name(): string {
238 return 'form_title';
239 }
240
241 /**
242 * Package-scoped confirmation-message string name.
243 *
244 * @param int $index Confirmation index.
245 * @since 2.11.0
246 * @return string
247 */
248 public static function confirmation_name( int $index ): string {
249 return 'confirmation_' . $index . '_message';
250 }
251
252 /**
253 * Package-scoped notification-field string name.
254 *
255 * @param int $index Notification index.
256 * @param string $field Notification field (subject|body|from_name|reply_to).
257 * @since 2.11.0
258 * @return string
259 */
260 public static function notification_name( int $index, string $field ): string {
261 return 'notification_' . $index . '_' . $field;
262 }
263
264 /**
265 * Package-scoped restriction-message string name.
266 *
267 * @since 2.11.0
268 * @return string
269 */
270 public static function restriction_name(): string {
271 return 'restriction_message';
272 }
273
274 /**
275 * Package-scoped block-attribute string name.
276 *
277 * @param string $block_id Stable block identifier.
278 * @param string $attribute Attribute key.
279 * @since 2.11.0
280 * @return string
281 */
282 public static function block_attribute_name( string $block_id, string $attribute ): string {
283 return 'block_' . $block_id . '_' . $attribute;
284 }
285
286 /**
287 * Package-scoped block-option-label string name.
288 *
289 * @param string $block_id Stable block identifier.
290 * @param int $option_index Option index.
291 * @since 2.11.0
292 * @return string
293 */
294 public static function block_option_name( string $block_id, int $option_index ): string {
295 return 'block_' . $block_id . '_option_' . $option_index . '_label';
296 }
297
298 /**
299 * Translate a form's submit button text.
300 *
301 * @param int $form_id Form post ID.
302 * @param string $value Original value (used as fallback when no translation exists).
303 * @since 2.11.0
304 * @return string Translated value, or the original when no provider/translation is available.
305 */
306 public function translate_submit_button( int $form_id, string $value ): string {
307 if ( '' === $value ) {
308 return $value;
309 }
310
311 return $this->dispatch_package( $form_id, self::submit_button_name(), $value );
312 }
313
314 /**
315 * Translate a form's title (post title).
316 *
317 * @param int $form_id Form post ID.
318 * @param string $value Original title (used as fallback when no translation exists).
319 * @since 2.12.3
320 * @return string Translated title, or the original when no provider/translation is available.
321 */
322 public function translate_form_title( int $form_id, string $value ): string {
323 if ( '' === $value ) {
324 return $value;
325 }
326
327 return $this->dispatch_package( $form_id, self::title_name(), $value );
328 }
329
330 /**
331 * Translate a per-confirmation message.
332 *
333 * @param int $form_id Form post ID.
334 * @param int $index Zero-based index of the confirmation within the form's confirmation set.
335 * @param string $value Original value.
336 * @since 2.11.0
337 * @return string Translated value, or the original when no provider/translation is available.
338 */
339 public function translate_confirmation_message( int $form_id, int $index, string $value ): string {
340 if ( '' === $value ) {
341 return $value;
342 }
343
344 return $this->dispatch_package( $form_id, self::confirmation_name( $index ), $value );
345 }
346
347 /**
348 * Translate an email notification subject.
349 *
350 * @param int $form_id Form post ID.
351 * @param int $index Zero-based index of the notification within the form's notification set.
352 * @param string $value Original value.
353 * @since 2.11.0
354 * @return string Translated value, or the original when no provider/translation is available.
355 */
356 public function translate_notification_subject( int $form_id, int $index, string $value ): string {
357 if ( '' === $value ) {
358 return $value;
359 }
360
361 return $this->dispatch_package( $form_id, self::notification_name( $index, 'subject' ), $value );
362 }
363
364 /**
365 * Translate an email notification body.
366 *
367 * @param int $form_id Form post ID.
368 * @param int $index Zero-based index of the notification within the form's notification set.
369 * @param string $value Original value.
370 * @since 2.11.0
371 * @return string Translated value, or the original when no provider/translation is available.
372 */
373 public function translate_notification_body( int $form_id, int $index, string $value ): string {
374 if ( '' === $value ) {
375 return $value;
376 }
377
378 return $this->dispatch_package( $form_id, self::notification_name( $index, 'body' ), $value );
379 }
380
381 /**
382 * Translate an email notification "from name".
383 *
384 * @param int $form_id Form post ID.
385 * @param int $index Zero-based index of the notification within the form's notification set.
386 * @param string $value Original value.
387 * @since 2.11.0
388 * @return string Translated value, or the original when no provider/translation is available.
389 */
390 public function translate_notification_from_name( int $form_id, int $index, string $value ): string {
391 if ( '' === $value ) {
392 return $value;
393 }
394
395 return $this->dispatch_package( $form_id, self::notification_name( $index, 'from_name' ), $value );
396 }
397
398 /**
399 * Translate a single built-in validation message keyed by its srfm_* identifier.
400 *
401 * Falls back to the supplied `$value` (which is typically already the
402 * WP-locale-translated output of {@see Translatable::dynamic_messages()}),
403 * so existing `.mo`-based translations keep working when the active
404 * multilingual provider has no translation for the string.
405 *
406 * @param string $key Stable identifier (e.g., `srfm_valid_email`).
407 * @param string $value Already-i18n'd value to use as fallback.
408 * @since 2.11.0
409 * @return string Translated value, or `$value` when the provider has no
410 * better translation.
411 */
412 public function translate_validation_message( string $key, string $value ): string {
413 if ( '' === $key || '' === $value ) {
414 return $value;
415 }
416
417 $name = 'validation_' . $key;
418 return $this->dispatch( $value, $name );
419 }
420
421 /**
422 * Translate an associative array of validation messages in one shot.
423 *
424 * Convenience wrapper around {@see translate_validation_message()} for the
425 * frontend-localize call site. Preserves the original array's keys and
426 * leaves the structure intact when the provider is inactive.
427 *
428 * @param array<string, string> $messages Map of `srfm_*` keys to message values.
429 * @since 2.11.0
430 * @return array<string, string> Map with translated values.
431 */
432 public function translate_validation_messages( array $messages ): array {
433 $out = [];
434 foreach ( $messages as $key => $value ) {
435 if ( ! is_string( $key ) || ! is_string( $value ) ) {
436 $out[ $key ] = $value;
437 continue;
438 }
439 $out[ $key ] = $this->translate_validation_message( $key, $value );
440 }
441 return $out;
442 }
443
444 /**
445 * Translate the form restriction message.
446 *
447 * @param int $form_id Form post ID.
448 * @param string $value Original value.
449 * @since 2.11.0
450 * @return string Translated value, or the original when no provider/translation is available.
451 */
452 public function translate_restriction_message( int $form_id, string $value ): string {
453 if ( '' === $value ) {
454 return $value;
455 }
456
457 return $this->dispatch_package( $form_id, self::restriction_name(), $value );
458 }
459
460 /**
461 * Translate a scalar block attribute (label, placeholder, help, errorMsg, etc).
462 *
463 * @param int $form_id Form post ID.
464 * @param string $block_id Stable block identifier from the block's `block_id` attribute.
465 * @param string $attribute Attribute key (e.g., `label`, `placeholder`).
466 * @param string $value Original value.
467 * @since 2.11.0
468 * @return string Translated value, or the original when no provider/translation is available.
469 */
470 public function translate_block_attribute( int $form_id, string $block_id, string $attribute, string $value ): string {
471 if ( '' === $value || '' === $block_id || '' === $attribute ) {
472 return $value;
473 }
474
475 return $this->dispatch_package( $form_id, self::block_attribute_name( $block_id, $attribute ), $value );
476 }
477
478 /**
479 * Translate a single option label inside a dropdown / multi-choice block.
480 *
481 * @param int $form_id Form post ID.
482 * @param string $block_id Stable block identifier from the block's `block_id` attribute.
483 * @param int $option_index Zero-based index of the option within the block's options array.
484 * @param string $value Original label value.
485 * @since 2.11.0
486 * @return string Translated value, or the original when no provider/translation is available.
487 */
488 public function translate_block_option_label( int $form_id, string $block_id, int $option_index, string $value ): string {
489 if ( '' === $value || '' === $block_id ) {
490 return $value;
491 }
492
493 return $this->dispatch_package( $form_id, self::block_option_name( $block_id, $option_index ), $value );
494 }
495
496 /**
497 * Pre-translate a form's raw block markup and also return the parsed top-level
498 * blocks so the render path can derive its block count without re-parsing the
499 * rendered HTML.
500 *
501 * Parses the post content, walks every translatable SureForms block attribute
502 * (per {@see translatable_block_attributes()} and {@see translatable_option_blocks()}),
503 * substitutes the translated value where the active multilingual provider has one,
504 * and re-serialises the markup. Inner blocks are walked recursively.
505 *
506 * This is the canonical, idempotent entry point that every render path (the
507 * standard form markup, and Pro multi-step / conversational renderers) should
508 * call AFTER applying the `srfm_get_form_post_content` filter and AFTER any
509 * path-specific marker injection — never before.
510 *
511 * The translated markup is cached per (form, language, post-modified, content)
512 * in the object cache: `post_modified_gmt` auto-invalidates on every form edit,
513 * and the content hash guards against content-mutating filters (e.g. randomised
514 * question order). Acts as a pure pass-through — no cache access, no parse — when
515 * no provider is active or the content is empty.
516 *
517 * @param int $form_id Form post ID.
518 * @param string $post_content Raw block markup (typically `$post->post_content`).
519 * @param \WP_Post|null $post Form post, used only for cache invalidation via `post_modified_gmt`.
520 * @since 2.11.0
521 * @return array{0: string, 1: array<int|string, array<string, mixed>>} [ markup, parsed top-level blocks ].
522 */
523 public function translate_form_content_with_blocks( int $form_id, string $post_content, ?\WP_Post $post = null ): array {
524 $provider = Multilingual_Manager::get_instance()->provider();
525
526 // Single-language / no-provider path: pure pass-through, zero overhead.
527 if ( ! $provider->is_active() || '' === trim( $post_content ) ) {
528 return [ $post_content, [] ];
529 }
530
531 $language = $provider->current_language();
532 $modified = $post instanceof \WP_Post ? (string) $post->post_modified_gmt : '';
533 $cache_key = 'srfm_xl_form_' . $form_id . '_' . md5( $language . '|' . $modified . '|' . md5( $post_content ) );
534
535 $cached = wp_cache_get( $cache_key, 'srfm_multilingual' );
536 if ( is_string( $cached ) ) {
537 return [ $cached, parse_blocks( $cached ) ];
538 }
539
540 $blocks = parse_blocks( $post_content );
541
542 if ( empty( $blocks ) ) {
543 return [ $post_content, [] ];
544 }
545
546 $translated_blocks = $this->translate_blocks_recursive( $form_id, $blocks );
547
548 // $translated_blocks is the round-tripped output of parse_blocks() with only
549 // its scalar attribute values swapped, so serialize_blocks() is safe here.
550 // PHPStan's stricter shape declaration for serialize_blocks() can't see through
551 // the generic array<string, mixed> we use internally to keep the recursion
552 // type-stable across innerBlocks. The runtime shape is identical.
553 // @phpstan-ignore-next-line argument.type
554 $markup = serialize_blocks( $translated_blocks );
555
556 wp_cache_set( $cache_key, $markup, 'srfm_multilingual', HOUR_IN_SECONDS );
557
558 return [ $markup, $translated_blocks ];
559 }
560
561 /**
562 * Back-compatible single-string entry point. Pre-translate a form's raw block
563 * markup before it reaches `do_blocks()`.
564 *
565 * Thin wrapper over {@see translate_form_content_with_blocks()} preserved for
566 * existing callers (including Pro) that only need the markup string. Idempotent:
567 * re-feeding already-translated markup is safe.
568 *
569 * @param int $form_id Form post ID.
570 * @param string $post_content Raw block markup (typically `$post->post_content`).
571 * @since 2.11.0
572 * @return string The (possibly translated) block markup.
573 */
574 public function translate_form_content( int $form_id, string $post_content ): string {
575 $post = get_post( $form_id );
576 [ $markup ] = $this->translate_form_content_with_blocks( $form_id, $post_content, $post instanceof \WP_Post ? $post : null );
577
578 return $markup;
579 }
580
581 /**
582 * Walk a list of parsed blocks (and their innerBlocks) translating attributes
583 * in-place. Block markers without recognised SureForms attributes pass through
584 * unchanged.
585 *
586 * @param int $form_id Form post ID.
587 * @param array<int|string, array<string, mixed>> $blocks Parsed-blocks structure.
588 * @since 2.11.0
589 * @return array<int|string, array<string, mixed>> The same structure with translatable attribute values swapped.
590 */
591 protected function translate_blocks_recursive( int $form_id, array $blocks ): array {
592 $attribute_map = self::translatable_block_attributes();
593 $option_blocks = self::translatable_option_blocks();
594
595 foreach ( $blocks as $index => $block ) {
596 $block_name = isset( $block['blockName'] ) && is_string( $block['blockName'] ) ? $block['blockName'] : '';
597 $attrs = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : [];
598 $block_id = isset( $attrs['block_id'] ) && is_string( $attrs['block_id'] ) ? $attrs['block_id'] : '';
599
600 // Translate scalar attributes for known SureForms blocks.
601 if ( '' !== $block_id && isset( $attribute_map[ $block_name ] ) ) {
602 foreach ( $attribute_map[ $block_name ] as $attribute_key ) {
603 if ( ! isset( $attrs[ $attribute_key ] ) || ! is_string( $attrs[ $attribute_key ] ) ) {
604 continue;
605 }
606 $attrs[ $attribute_key ] = $this->translate_block_attribute(
607 $form_id,
608 $block_id,
609 $attribute_key,
610 $attrs[ $attribute_key ]
611 );
612 }
613 }
614
615 // Translate option labels for dropdown / multi-choice.
616 if ( '' !== $block_id && in_array( $block_name, $option_blocks, true ) && isset( $attrs['options'] ) && is_array( $attrs['options'] ) ) {
617 foreach ( $attrs['options'] as $option_index => $option ) {
618 if ( ! is_array( $option ) || ! isset( $option['label'] ) || ! is_string( $option['label'] ) ) {
619 continue;
620 }
621 $attrs['options'][ $option_index ]['label'] = $this->translate_block_option_label(
622 $form_id,
623 $block_id,
624 (int) $option_index,
625 $option['label']
626 );
627 }
628 }
629
630 $blocks[ $index ]['attrs'] = $attrs;
631
632 // Recurse into innerBlocks (e.g., page-break wrappers).
633 if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
634 $blocks[ $index ]['innerBlocks'] = $this->translate_blocks_recursive( $form_id, $block['innerBlocks'] );
635 }
636 }
637
638 return $blocks;
639 }
640
641 /**
642 * Dispatch the translate call to the active multilingual provider.
643 *
644 * @param string $value Original value (used as fallback when no translation exists).
645 * @param string $name Fully built string name per the documented naming scheme.
646 * @since 2.11.0
647 * @return string Translated value, or the original value when the provider has no translation.
648 */
649 private function dispatch( string $value, string $name ): string {
650 $provider = Multilingual_Manager::get_instance()->provider();
651 return $provider->translate( $value, $name, self::DOMAIN );
652 }
653
654 /**
655 * Translate a per-form string via the provider's String Package, so it shows
656 * grouped under the form in the Translation Editor.
657 *
658 * Falls back to a flat String-Translation string (legacy `form_{id}_{name}`
659 * naming) when the provider can't do packages — keeping older WPML setups and
660 * non-package providers working.
661 *
662 * @param int $form_id Form post ID.
663 * @param string $name Package-scoped string name (see the *_name() builders).
664 * @param string $value Original value (fallback when untranslated).
665 * @since 2.11.0
666 * @return string Translated value, or the original when no translation exists.
667 */
668 private function dispatch_package( int $form_id, string $name, string $value ): string {
669 if ( '' === $value ) {
670 return $value;
671 }
672
673 $provider = Multilingual_Manager::get_instance()->provider();
674
675 if ( $provider->supports_packages() ) {
676 return $provider->translate_package_string( self::form_package( $form_id ), $name, $value );
677 }
678
679 // Legacy / non-package fallback: flat string with the form-scoped name.
680 return $provider->translate( $value, 'form_' . $form_id . '_' . $name, self::DOMAIN );
681 }
682 }
683