PluginProbe
Polylang / trunk
Polylang vtrunk
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 trunk, at src/settings/table-string.php

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