PluginProbe
Polylang / 3.4.2
Polylang v3.4.2
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 / settings / table-string.php

table-string.php in Polylang 3.4.2, at settings/table-string.php

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