PluginProbe
Polylang / 3.8.5
Polylang v3.8.5
3.8.9 3.8.8 3.8.7 3.8.6 3.8.5 3.8.4 3.8.3 2.7 2.7.0.1 2.7.1 2.7.2 2.7.3 2.7.4 2.8 2.8.1 2.8.2 2.8.3 2.8.4 2.9 2.9.1 2.9.2 3.0 3.0.1 3.0.2 3.0.3 All 233 releases
polylang / src / settings / table-string.php

table-string.php in Polylang 3.8.5, at src/settings/table-string.php

474 lines 14.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Polylang
4 */
5
6 use WP_Syntex\Polylang\Model\Languages;
7
8 if ( ! class_exists( 'WP_List_Table' ) ) {
9 require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; // since WP 3.1
10 }
11
12 /**
13 * A class to create the strings translations table
14 * Thanks to Matt Van Andel ( http://www.mattvanandel.com ) for its plugin "Custom List Table Example" !
15 *
16 * @since 0.6
17 */
18 class PLL_Table_String extends WP_List_Table {
19 /**
20 * Languages model.
21 *
22 * @var Languages
23 */
24 protected $languages;
25
26 /**
27 * Registered strings.
28 *
29 * @var array
30 */
31 protected $strings;
32
33 /**
34 * The string groups.
35 *
36 * @var string[]
37 */
38 protected $groups;
39
40 /**
41 * The selected string group or -1 if none is selected.
42 *
43 * @var string|int
44 */
45 protected $selected_group;
46
47 /**
48 * Constructor.
49 *
50 * @since 0.6
51 * @since 3.8 The 1st argument is an instance of the languages model.
52 *
53 * @param Languages $languages Languages model.
54 */
55 public function __construct( Languages $languages ) {
56 parent::__construct(
57 array(
58 'plural' => 'Strings translations', // Do not translate ( used for css class )
59 'ajax' => false,
60 )
61 );
62
63 $this->languages = $languages;
64 $this->strings = PLL_Admin_Strings::get_strings();
65 $this->groups = array_unique( wp_list_pluck( $this->strings, 'context' ) );
66
67 $this->selected_group = -1;
68
69 if ( ! empty( $_GET['group'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
70 $group = sanitize_text_field( wp_unslash( $_GET['group'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
71 if ( in_array( $group, $this->groups ) ) {
72 $this->selected_group = $group;
73 }
74 }
75
76 add_action( 'mlang_action_string-translation', array( $this, 'save_translations' ) );
77 }
78
79 /**
80 * Displays the item information in a column (default case).
81 *
82 * @since 0.6
83 *
84 * @param array $item Data related to the current string.
85 * @param string $column_name The current column name.
86 * @return string
87 */
88 public function column_default( $item, $column_name ) {
89 return $item[ $column_name ];
90 }
91
92 /**
93 * Displays the checkbox in first column.
94 *
95 * @since 1.1
96 *
97 * @param array $item Data related to the current string.
98 * @return string
99 */
100 public function column_cb( $item ) {
101 return sprintf(
102 '<label class="screen-reader-text" for="cb-select-%1$s">%2$s</label><input id="cb-select-%1$s" type="checkbox" name="strings[]" value="%1$s" %3$s />',
103 esc_attr( $item['row'] ),
104 /* translators: accessibility text, %s is a string potentially in any language */
105 sprintf( __( 'Select %s', 'polylang' ), format_to_edit( $item['string'] ) ),
106 disabled( empty( $item['icl'] ), true, false ) // Only strings registered with WPML API can be removed.
107 );
108 }
109
110 /**
111 * Displays the string to translate.
112 *
113 * @since 1.0
114 *
115 * @param array $item Data related to the current string.
116 * @return string
117 */
118 public function column_string( $item ) {
119 return format_to_edit( $item['string'] ); // Don't interpret special chars for the string column.
120 }
121
122 /**
123 * Displays the translations to edit.
124 *
125 * @since 0.6
126 *
127 * @param array $item Data related to the current string.
128 * @return string
129 */
130 public function column_translations( $item ) {
131 $out = '';
132 $languages = array();
133
134 foreach ( $this->languages->get_list() as $language ) {
135 $languages[ $language->slug ] = $language->name;
136 }
137
138 foreach ( $item['translations'] as $key => $translation ) {
139 $input_type = $item['multiline'] ?
140 '<textarea name="translation[%1$s][%2$s]" id="%1$s-%2$s" %5$s>%4$s</textarea>' :
141 '<input type="text" name="translation[%1$s][%2$s]" id="%1$s-%2$s" value="%4$s" %5$s/>';
142
143 $out .= sprintf(
144 '<div class="translation"><label for="%1$s-%2$s">%3$s</label>' . $input_type . "</div>\n",
145 esc_attr( $key ),
146 esc_attr( $item['row'] ),
147 esc_html( $languages[ $key ] ),
148 format_to_edit( $translation ), // Don't interpret special chars.
149 $item['disabled'][ $key ]
150 );
151 }
152
153 return $out;
154 }
155
156 /**
157 * Gets the list of columns.
158 *
159 * @since 0.6
160 *
161 * @return string[] The list of column titles.
162 */
163 public function get_columns() {
164 return array(
165 'cb' => '<input type="checkbox" />', // Checkbox.
166 'string' => esc_html__( 'String', 'polylang' ),
167 'name' => esc_html__( 'Name', 'polylang' ),
168 'context' => esc_html__( 'Group', 'polylang' ),
169 'translations' => esc_html__( 'Translations', 'polylang' ),
170 );
171 }
172
173 /**
174 * Gets the list of sortable columns
175 *
176 * @since 0.6
177 *
178 * @return array
179 */
180 public function get_sortable_columns() {
181 return array(
182 'string' => array( 'string', false ),
183 'name' => array( 'name', false ),
184 'context' => array( 'context', false ),
185 );
186 }
187
188 /**
189 * Gets the name of the default primary column.
190 *
191 * @since 2.1
192 *
193 * @return string Name of the default primary column, in this case, 'string'.
194 */
195 protected function get_default_primary_column_name() {
196 return 'string';
197 }
198
199 /**
200 * Search for a string in translations. Case insensitive.
201 *
202 * @since 2.6
203 *
204 * @param PLL_Language[] $languages An array of language objects.
205 * @param string $s Searched string.
206 * @return string[] Found strings.
207 */
208 protected function search_in_translations( $languages, $s ) {
209 $founds = array();
210
211 foreach ( $languages as $language ) {
212 $mo = new PLL_MO();
213 $mo->import_from_db( $language );
214 foreach ( wp_list_pluck( $mo->entries, 'translations' ) as $string => $translation ) {
215 if ( false !== stripos( $translation[0], $s ) ) {
216 $founds[] = $string;
217 }
218 }
219 }
220
221 return array_unique( $founds );
222 }
223
224 /**
225 * Sorts registered string items.
226 *
227 * @since 0.6
228 *
229 * @param array $a The first item to compare.
230 * @param array $b The second item to compare.
231 * @return int -1 or 1 if $a is considered to be respectively less than or greater than $b.
232 */
233 protected function usort_reorder( $a, $b ) {
234 if ( ! empty( $_GET['orderby'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
235 $orderby = sanitize_key( $_GET['orderby'] ); // phpcs:ignore WordPress.Security.NonceVerification
236 if ( isset( $a[ $orderby ], $b[ $orderby ] ) ) {
237 $result = strcmp( $a[ $orderby ], $b[ $orderby ] ); // Determine sort order
238 return ( empty( $_GET['order'] ) || 'asc' === $_GET['order'] ) ? $result : -$result; // phpcs:ignore WordPress.Security.NonceVerification
239 }
240 }
241
242 return 0;
243 }
244
245 /**
246 * Prepares the list of registered strings for display.
247 *
248 * @since 0.6
249 *
250 * @return void
251 */
252 public function prepare_items() {
253 $languages = $this->languages->get_list();
254
255 // Is admin language filter active?
256 $filter = get_user_meta( get_current_user_id(), 'pll_filter_content', true );
257 if ( $filter ) {
258 $languages = wp_list_filter( $languages, array( 'slug' => $filter ) );
259 }
260
261 $data = $this->strings;
262
263 // Filter by selected group.
264 if ( -1 !== $this->selected_group ) {
265 $data = wp_list_filter( $data, array( 'context' => $this->selected_group ) );
266 }
267
268 // Filter by searched string.
269 $s = empty( $_GET['s'] ) ? '' : wp_unslash( $_GET['s'] ); // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
270
271 if ( ! empty( $s ) ) {
272 // Search in translations.
273 $in_translations = $this->search_in_translations( $languages, $s );
274
275 foreach ( $data as $key => $row ) {
276 if ( stripos( $row['name'], $s ) === false && stripos( $row['string'], $s ) === false && ! in_array( $row['string'], $in_translations ) ) {
277 unset( $data[ $key ] );
278 }
279 }
280 }
281
282 // Sorting.
283 uasort( $data, array( $this, 'usort_reorder' ) );
284
285 // Paging.
286 $per_page = $this->get_items_per_page( 'pll_strings_per_page' );
287 $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() );
288
289 $total_items = count( $data );
290 $this->items = array_slice( $data, ( $this->get_pagenum() - 1 ) * $per_page, $per_page, true );
291
292 $this->set_pagination_args(
293 array(
294 'total_items' => $total_items,
295 'per_page' => $per_page,
296 'total_pages' => (int) ceil( $total_items / $per_page ),
297 )
298 );
299
300 $allowed_language_slugs = $this->languages->filter( 'translator' )->get_list( array( 'fields' => 'slug' ) );
301
302 /*
303 * Translate strings.
304 * Kept for the end as it is a slow process.
305 */
306 foreach ( $languages as $language ) {
307 $disabled = disabled( in_array( $language->slug, $allowed_language_slugs, true ), false, false );
308
309 $mo = new PLL_MO();
310 $mo->import_from_db( $language );
311 foreach ( $this->items as $key => $row ) {
312 $this->items[ $key ]['translations'][ $language->slug ] = $mo->translate_if_any( $row['string'] );
313 $this->items[ $key ]['row'] = $key; // Store the row number for convenience.
314 $this->items[ $key ]['disabled'][ $language->slug ] = $disabled;
315 }
316 }
317 }
318
319 /**
320 * Get the list of possible bulk actions.
321 *
322 * @since 1.1
323 *
324 * @return string[] Array of bulk actions.
325 */
326 public function get_bulk_actions() {
327 if ( current_user_can( 'manage_options' ) ) {
328 return array( 'delete' => __( 'Delete', 'polylang' ) );
329 }
330 return array();
331 }
332
333 /**
334 * Get the current action selected from the bulk actions dropdown.
335 * overrides parent function to avoid submit button to trigger bulk actions
336 *
337 * @since 1.8
338 *
339 * @return string|false The action name or False if no action was selected
340 */
341 public function current_action() {
342 return empty( $_POST['submit'] ) ? parent::current_action() : false; // phpcs:ignore WordPress.Security.NonceVerification
343 }
344
345 /**
346 * Displays the dropdown list to filter strings per group
347 *
348 * @since 1.1
349 *
350 * @param string $which only 'top' is supported
351 * @return void
352 */
353 public function extra_tablenav( $which ) {
354 if ( 'top' !== $which ) {
355 return;
356 }
357
358 echo '<div class="alignleft actions">';
359 printf(
360 '<label class="screen-reader-text" for="select-group" >%s</label>',
361 /* translators: accessibility text */
362 esc_html__( 'Filter by group', 'polylang' )
363 );
364 echo '<select id="select-group" name="group">' . "\n";
365 printf(
366 '<option value="-1"%s>%s</option>' . "\n",
367 selected( $this->selected_group, -1, false ),
368 esc_html__( 'View all groups', 'polylang' )
369 );
370
371 foreach ( $this->groups as $group ) {
372 printf(
373 '<option value="%s"%s>%s</option>' . "\n",
374 esc_attr( urlencode( $group ) ),
375 selected( $this->selected_group, $group, false ),
376 esc_html( $group )
377 );
378 }
379 echo '</select>' . "\n";
380
381 submit_button( __( 'Filter', 'polylang' ), 'button', 'filter_action', false, array( 'id' => 'post-query-submit' ) );
382 echo '</div>';
383 }
384
385 /**
386 * Saves the strings translations in DB
387 * Optionally clean the DB
388 *
389 * @since 1.9
390 *
391 * @return void
392 *
393 * @phpstan-return never
394 */
395 public function save_translations() {
396 check_admin_referer( 'string-translation', '_wpnonce_string-translation' );
397
398 if ( ! empty( $_POST['submit'] ) ) {
399 foreach ( $this->languages->filter( 'translator' )->get_list() as $language ) {
400 if ( empty( $_POST['translation'][ $language->slug ] ) || ! is_array( $_POST['translation'][ $language->slug ] ) ) { // In case the language filter is active ( thanks to John P. Bloch )
401 continue;
402 }
403
404 $translations = array_map( 'trim', (array) wp_unslash( $_POST['translation'][ $language->slug ] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
405
406 $mo = new PLL_MO();
407 $mo->import_from_db( $language );
408
409 foreach ( $translations as $key => $translation ) {
410 /**
411 * Filters the string translation before it is saved in DB.
412 * Allows to sanitize strings registered with pll_register_string().
413 *
414 * @since 1.6
415 * @since 2.7 The translation passed to the filter is unslashed.
416 * @since 3.7 Add original string as 4th parameter.
417 * @since 3.8 Add previous string translation as 5th parameter.
418 *
419 * @param string $translation The string translation.
420 * @param string $name The name as defined in pll_register_string.
421 * @param string $context The context as defined in pll_register_string.
422 * @param string $original The original string to translate.
423 * @param string $previous The previous string translation.
424 */
425 $translation = apply_filters( 'pll_sanitize_string_translation', $translation, $this->strings[ $key ]['name'], $this->strings[ $key ]['context'], $this->strings[ $key ]['string'], $mo->translate_if_any( $this->strings[ $key ]['string'] ) );
426 $mo->add_entry(
427 $mo->make_entry(
428 $this->strings[ $key ]['string'],
429 $translation
430 )
431 );
432 }
433
434 // Clean database (removes all strings which were registered some day but are no more).
435 if ( ! empty( $_POST['clean'] ) && current_user_can( 'manage_options' ) ) {
436 $new_mo = new PLL_MO();
437
438 foreach ( $this->strings as $string ) {
439 $new_mo->add_entry( $mo->make_entry( $string['string'], $mo->translate( $string['string'] ) ) );
440 }
441 }
442
443 isset( $new_mo ) ? $new_mo->export_to_db( $language ) : $mo->export_to_db( $language );
444 }
445
446 pll_add_notice( new WP_Error( 'pll_strings_translations_updated', __( 'Translations updated.', 'polylang' ), 'success' ) );
447
448 /**
449 * Fires after the strings translations are saved in DB
450 *
451 * @since 1.2
452 */
453 do_action( 'pll_save_strings_translations' );
454 }
455
456 // Unregisters strings registered through WPML API
457 if ( $this->current_action() === 'delete' && ! empty( $_POST['strings'] ) && function_exists( 'icl_unregister_string' ) && current_user_can( 'manage_options' ) ) {
458 foreach ( array_map( 'sanitize_key', $_POST['strings'] ) as $key ) {
459 icl_unregister_string( $this->strings[ $key ]['context'], $this->strings[ $key ]['name'] );
460 }
461 }
462
463 // To refresh the page ( possible thanks to the $_GET['noheader']=true )
464 $args = array_intersect_key( $_REQUEST, array_flip( array( 's', 'paged', 'group' ) ) );
465 if ( ! empty( $_GET['paged'] ) && ! empty( $_POST['submit'] ) ) {
466 $args['paged'] = (int) $_GET['paged']; // Don't rely on $_REQUEST['paged'] or $_POST['paged']. See #14
467 }
468 if ( ! empty( $args['s'] ) ) {
469 $args['s'] = urlencode( $args['s'] ); // Searched string needs to be encoded as it comes from $_POST
470 }
471 PLL_Settings::redirect( $args );
472 }
473 }
474