PluginProbe ʕ •ᴥ•ʔ
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / trunk
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz vtrunk
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 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8 0.0.9 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.1.0 1.1.1 1.1.2 1.10.0 1.10.1 1.11.0 1.12.0 1.12.1 1.12.2 1.12.3 1.13.0 1.13.1 1.13.2 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.8.0 1.9.0 1.9.1 2.0.0 2.0.1 2.0.2 2.1.0 2.1.1 2.2.0 2.2.1 2.2.2 2.3.0 2.4.0 2.5.0 2.5.2 2.6.0
sureforms / inc / compatibility / multilingual / providers / wpml-provider.php
sureforms / inc / compatibility / multilingual / providers Last commit date
null-provider.php 3 weeks ago provider.php 3 weeks ago wpml-provider.php 3 weeks ago
wpml-provider.php
407 lines
1 <?php
2 /**
3 * WPML Multilingual Provider.
4 *
5 * Adapter that bridges SureForms to the WPML plugin via its public hook surface.
6 * Requires WPML 4.5+; older versions are treated as inactive.
7 *
8 * @package sureforms.
9 * @since 2.11.0
10 */
11
12 namespace SRFM\Inc\Compatibility\Multilingual\Providers;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit; // Exit if accessed directly.
16 }
17
18 // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- This adapter exists solely to call WPML's own hooks (wpml_*); their names must match WPML exactly to integrate.
19
20 /**
21 * WPML_Provider.
22 *
23 * Implements {@see Provider} on top of WPML's filter/action API.
24 *
25 * @since 2.11.0
26 */
27 class WPML_Provider implements Provider {
28 /**
29 * Minimum supported WPML version.
30 *
31 * @since 2.11.0
32 */
33 public const MIN_WPML_VERSION = '4.5';
34
35 /**
36 * Memoized active state. Null means "not yet computed".
37 *
38 * @since 2.11.0
39 * @var bool|null
40 */
41 private $is_active_cache = null;
42
43 /**
44 * Stack of languages pushed by {@see switch_language()}, used to restore previous context.
45 *
46 * @since 2.11.0
47 * @var array<int, string>
48 */
49 private $previous_language = [];
50
51 /**
52 * Whether WPML is active and at the minimum supported version.
53 *
54 * @since 2.11.0
55 * @return bool True when WPML 4.5+ is available, false otherwise.
56 */
57 public function is_active(): bool {
58 if ( null !== $this->is_active_cache ) {
59 return $this->is_active_cache;
60 }
61
62 $active = defined( 'ICL_SITEPRESS_VERSION' )
63 && class_exists( '\SitePress' )
64 && version_compare( (string) constant( 'ICL_SITEPRESS_VERSION' ), self::MIN_WPML_VERSION, '>=' );
65
66 $this->is_active_cache = $active;
67 return $this->is_active_cache;
68 }
69
70 /**
71 * Current visitor language code as reported by WPML.
72 *
73 * @since 2.11.0
74 * @return string Language code, or empty string when WPML is inactive or the filter returns a non-string.
75 */
76 public function current_language(): string {
77 if ( ! $this->is_active() ) {
78 return '';
79 }
80
81 $language = apply_filters( 'wpml_current_language', null );
82 return is_string( $language ) ? $language : '';
83 }
84
85 /**
86 * Site default language code as reported by WPML.
87 *
88 * @since 2.11.0
89 * @return string Language code, or empty string when WPML is inactive or the filter returns a non-string.
90 */
91 public function default_language(): string {
92 if ( ! $this->is_active() ) {
93 return '';
94 }
95
96 $language = apply_filters( 'wpml_default_language', null );
97 return is_string( $language ) ? $language : '';
98 }
99
100 /**
101 * Register a string with WPML's String Translation registry.
102 *
103 * @param string $name Unique string identifier within the domain.
104 * @param string $value Original string value to register.
105 * @param string $domain Translation domain. Defaults to the sureforms text domain.
106 * @since 2.11.0
107 * @return void
108 */
109 public function register_string( string $name, string $value, string $domain = 'sureforms' ): void {
110 if ( ! $this->is_active() ) {
111 return;
112 }
113
114 do_action( 'wpml_register_single_string', $domain, $name, $value );
115 }
116
117 /**
118 * Translate a registered string via WPML.
119 *
120 * @param string $value Original string value (used as fallback).
121 * @param string $name Unique string identifier within the domain.
122 * @param string $domain Translation domain. Defaults to the sureforms text domain.
123 * @param string|null $language Optional target language code. When null, uses the current language.
124 * @since 2.11.0
125 * @return string Translated value, or the original $value when WPML is inactive or no translation exists.
126 */
127 public function translate( string $value, string $name, string $domain = 'sureforms', ?string $language = null ): string {
128 if ( ! $this->is_active() ) {
129 return $value;
130 }
131
132 if ( null !== $language ) {
133 $translated = apply_filters( 'wpml_translate_single_string', $value, $domain, $name, $language );
134 } else {
135 $translated = apply_filters( 'wpml_translate_single_string', $value, $domain, $name );
136 }
137
138 if ( null === $translated || ! is_string( $translated ) ) {
139 return $value;
140 }
141
142 return $translated;
143 }
144
145 /**
146 * Push the current language onto an internal stack and switch WPML to $language.
147 *
148 * @param string $language Target language code to switch to.
149 * @since 2.11.0
150 * @return void
151 */
152 public function switch_language( string $language ): void {
153 if ( ! $this->is_active() ) {
154 return;
155 }
156
157 $this->previous_language[] = $this->current_language();
158 do_action( 'wpml_switch_language', $language );
159 }
160
161 /**
162 * Pop the previous language off the internal stack and restore WPML's context.
163 *
164 * @since 2.11.0
165 * @return void
166 */
167 public function restore_language(): void {
168 if ( ! $this->is_active() ) {
169 return;
170 }
171
172 if ( empty( $this->previous_language ) ) {
173 return;
174 }
175
176 $previous = array_pop( $this->previous_language );
177 do_action( 'wpml_switch_language', $previous );
178 }
179
180 /**
181 * {@inheritDoc}
182 *
183 * Builds a self-contained list-based switcher from WPML's `wpml_active_languages`
184 * data. Doesn't depend on the site's WPML "switcher slot" being enabled (footer,
185 * post-actions, etc.), so it works on custom templates that don't include the
186 * theme footer — like SureForms' single-form.php instant-form template.
187 *
188 * Returns empty when WPML is inactive or fewer than two languages are configured.
189 *
190 * @since 2.11.0
191 * @return string Rendered switcher HTML, or empty string.
192 */
193 public function render_language_switcher(): string {
194 if ( ! $this->is_active() ) {
195 return '';
196 }
197
198 $languages = $this->get_active_languages();
199 if ( count( $languages ) < 2 ) {
200 return '';
201 }
202
203 $current = $this->current_language();
204
205 $items = '';
206 foreach ( $languages as $code => $language ) {
207 $url = isset( $language['url'] ) && is_string( $language['url'] ) ? $language['url'] : '';
208 $native = isset( $language['native_name'] ) && is_string( $language['native_name'] ) ? $language['native_name'] : '';
209 $lang = isset( $language['language_code'] ) && is_string( $language['language_code'] ) ? $language['language_code'] : (string) $code;
210
211 if ( '' === $url || '' === $native ) {
212 continue;
213 }
214
215 // Prefer WPML's own `active` flag from wpml_active_languages (the canonical
216 // current-language signal for the switcher), falling back to comparing against
217 // current_language() when the flag isn't present.
218 $is_current = isset( $language['active'] ) ? ! empty( $language['active'] ) : ( $lang === $current );
219 $class = $is_current ? 'srfm-lang-item srfm-lang-item-current' : 'srfm-lang-item';
220 $items .= sprintf(
221 '<li class="%1$s"><a href="%2$s" hreflang="%3$s" lang="%3$s">%4$s</a></li>',
222 esc_attr( $class ),
223 esc_url( $url ),
224 esc_attr( $lang ),
225 esc_html( $native )
226 );
227 }
228
229 if ( '' === $items ) {
230 return '';
231 }
232
233 return '<ul class="srfm-lang-switcher-list" role="navigation" aria-label="' . esc_attr__( 'Language Switcher', 'sureforms' ) . '">' . $items . '</ul>';
234 }
235
236 /**
237 * Whether WPML's String Package API is available.
238 *
239 * Package translation is provided by the WPML String Translation plugin's
240 * package module. Gate on the `wpml_register_string` action so we degrade
241 * gracefully (callers fall back to flat strings) if only WPML core is active.
242 *
243 * @since 2.11.0
244 * @return bool
245 */
246 public function supports_packages(): bool {
247 return $this->is_active() && has_action( 'wpml_register_string' );
248 }
249
250 /**
251 * {@inheritDoc}
252 *
253 * @param array<string,string> $package Package descriptor (kind, name, title, edit_link).
254 * @since 2.11.0
255 * @return void
256 */
257 public function start_package( array $package ): void {
258 if ( ! $this->supports_packages() ) {
259 return;
260 }
261 do_action( 'wpml_start_string_package_registration', $package );
262 }
263
264 /**
265 * {@inheritDoc}
266 *
267 * @param array<string,string> $package Package descriptor.
268 * @since 2.11.0
269 * @return void
270 */
271 public function finish_package( array $package ): void {
272 if ( ! $this->supports_packages() ) {
273 return;
274 }
275 do_action( 'wpml_delete_unused_package_strings', $package );
276 }
277
278 /**
279 * {@inheritDoc}
280 *
281 * @param array<string,string> $package Package descriptor.
282 * @param string $name String identifier within the package.
283 * @param string $value Original value.
284 * @param string $title Editor label.
285 * @param string $type Editor field type (LINE|AREA|VISUAL).
286 * @since 2.11.0
287 * @return void
288 */
289 public function register_package_string( array $package, string $name, string $value, string $title = '', string $type = 'LINE' ): void {
290 if ( ! $this->supports_packages() ) {
291 return;
292 }
293 do_action( 'wpml_register_string', $value, $name, $package, '' !== $title ? $title : $name, $type );
294 }
295
296 /**
297 * {@inheritDoc}
298 *
299 * @param array<string,string> $package Package descriptor.
300 * @param string $name String identifier within the package.
301 * @param string $value Original value (fallback).
302 * @since 2.11.0
303 * @return string
304 */
305 public function translate_package_string( array $package, string $name, string $value ): string {
306 if ( ! $this->supports_packages() ) {
307 return $value;
308 }
309 $translated = apply_filters( 'wpml_translate_string', $value, $name, $package );
310 return is_string( $translated ) ? $translated : $value;
311 }
312
313 /**
314 * {@inheritDoc}
315 *
316 * Removes the package and its translations from WPML via the
317 * `wpml_delete_package` action, keyed by the package name + kind.
318 *
319 * @param array<string,string> $package Package descriptor.
320 * @since 2.12.3
321 * @return void
322 */
323 public function delete_package( array $package ): void {
324 if ( ! $this->supports_packages() ) {
325 return;
326 }
327
328 $name = $package['name'] ?? '';
329 $kind = $package['kind'] ?? '';
330
331 if ( '' === $name || '' === $kind ) {
332 return;
333 }
334
335 do_action( 'wpml_delete_package', $name, $kind );
336 }
337
338 /**
339 * Build the active-language list from the most reliable WPML surface available.
340 *
341 * Tries `apply_filters( 'wpml_active_languages', ... )` first (the documented
342 * public API that returns URLs for the current page). When that returns no
343 * data — which happens on custom template paths that run before WPML has
344 * fully bootstrapped its language switcher — falls back to SitePress's
345 * internal `get_active_languages()` data and constructs URLs via the
346 * `wpml_permalink` filter so the switcher still links to translated copies.
347 *
348 * @since 2.11.0
349 * @return array<string, array<string, mixed>> Map of language code → {url, native_name, language_code}.
350 */
351 protected function get_active_languages(): array {
352 $languages = apply_filters( 'wpml_active_languages', null, 'skip_missing=0' );
353 if ( is_array( $languages ) && ! empty( $languages ) ) {
354 return $languages;
355 }
356
357 global $sitepress;
358 if ( ! is_object( $sitepress ) || ! method_exists( $sitepress, 'get_active_languages' ) ) {
359 return [];
360 }
361
362 $sp_languages = $sitepress->get_active_languages();
363 if ( ! is_array( $sp_languages ) || empty( $sp_languages ) ) {
364 return [];
365 }
366
367 $current_url = $this->guess_current_url();
368 $out = [];
369 foreach ( $sp_languages as $code => $data ) {
370 if ( ! is_string( $code ) || '' === $code || ! is_array( $data ) ) {
371 continue;
372 }
373 $native = isset( $data['native_name'] ) && is_string( $data['native_name'] ) ? $data['native_name'] : $code;
374
375 $url = apply_filters( 'wpml_permalink', $current_url, $code );
376 if ( ! is_string( $url ) || '' === $url ) {
377 continue;
378 }
379
380 $out[ $code ] = [
381 'language_code' => $code,
382 'native_name' => $native,
383 'url' => $url,
384 ];
385 }
386 return $out;
387 }
388
389 /**
390 * Best-effort current page URL for use with the `wpml_permalink` filter when
391 * SitePress hasn't pre-computed per-language URLs for the request.
392 *
393 * @since 2.11.0
394 * @return string Current request URL, or home URL as a last resort.
395 */
396 protected function guess_current_url(): string {
397 if ( function_exists( 'home_url' ) ) {
398 $uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
399 if ( '' !== $uri ) {
400 return home_url( $uri );
401 }
402 return home_url( '/' );
403 }
404 return '';
405 }
406 }
407