PluginProbe
Polylang / 3.0.4
Polylang v3.0.4
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.0.4, at settings/table-string.php

445 lines 12.7 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 $languages = array_combine( wp_list_pluck( $this->languages, 'slug' ), wp_list_pluck( $this->languages, 'name' ) );
129 $out = '';
130
131 foreach ( $item['translations'] as $key => $translation ) {
132 $input_type = $item['multiline'] ?
133 '<textarea name="translation[%1$s][%2$s]" id="%1$s-%2$s">%4$s</textarea>' :
134 '<input type="text" name="translation[%1$s][%2$s]" id="%1$s-%2$s" value="%4$s" />';
135 $out .= sprintf(
136 '<div class="translation"><label for="%1$s-%2$s">%3$s</label>' . $input_type . '</div>' . "\n",
137 esc_attr( $key ),
138 esc_attr( $item['row'] ),
139 esc_html( $languages[ $key ] ),
140 format_to_edit( $translation ) // Don't interpret special chars
141 );
142 }
143
144 return $out;
145 }
146
147 /**
148 * Gets the list of columns.
149 *
150 * @since 0.6
151 *
152 * @return string[] The list of column titles.
153 */
154 public function get_columns() {
155 return array(
156 'cb' => '<input type="checkbox" />', // Checkbox.
157 'string' => esc_html__( 'String', 'polylang' ),
158 'name' => esc_html__( 'Name', 'polylang' ),
159 'context' => esc_html__( 'Group', 'polylang' ),
160 'translations' => esc_html__( 'Translations', 'polylang' ),
161 );
162 }
163
164 /**
165 * Gets the list of sortable columns
166 *
167 * @since 0.6
168 *
169 * @return array
170 */
171 public function get_sortable_columns() {
172 return array(
173 'string' => array( 'string', false ),
174 'name' => array( 'name', false ),
175 'context' => array( 'context', false ),
176 );
177 }
178
179 /**
180 * Gets the name of the default primary column.
181 *
182 * @since 2.1
183 *
184 * @return string Name of the default primary column, in this case, 'string'.
185 */
186 protected function get_default_primary_column_name() {
187 return 'string';
188 }
189
190 /**
191 * Search for a string in translations. Case insensitive.
192 *
193 * @since 2.6
194 *
195 * @param PLL_MO[] $mos An array of PLL_MO objects.
196 * @param string $s Searched string.
197 * @return string[] Found strings.
198 */
199 protected function search_in_translations( $mos, $s ) {
200 $founds = array();
201
202 foreach ( $mos as $mo ) {
203 foreach ( wp_list_pluck( $mo->entries, 'translations' ) as $string => $translation ) {
204 if ( false !== stripos( $translation[0], $s ) ) {
205 $founds[] = $string;
206 }
207 }
208 }
209
210 return array_unique( $founds );
211 }
212
213 /**
214 * Sort items
215 *
216 * @since 0.6
217 *
218 * @param object $a The first object to compare
219 * @param object $b The second object to compare
220 * @return int -1 or 1 if $a is considered to be respectively less than or greater than $b.
221 */
222 protected function usort_reorder( $a, $b ) {
223 if ( ! empty( $_GET['orderby'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
224 $orderby = sanitize_key( $_GET['orderby'] ); // phpcs:ignore WordPress.Security.NonceVerification
225 if ( isset( $a[ $orderby ], $b[ $orderby ] ) ) {
226 $result = strcmp( $a[ $orderby ], $b[ $orderby ] ); // Determine sort order
227 return ( empty( $_GET['order'] ) || 'asc' === $_GET['order'] ) ? $result : -$result; // phpcs:ignore WordPress.Security.NonceVerification
228 }
229 }
230
231 return 0;
232 }
233
234 /**
235 * Prepares the list of items for displaying
236 *
237 * @since 0.6
238 *
239 * @return void
240 */
241 public function prepare_items() {
242 // Is admin language filter active?
243 if ( $lg = get_user_meta( get_current_user_id(), 'pll_filter_content', true ) ) {
244 $languages = wp_list_filter( $this->languages, array( 'slug' => $lg ) );
245 } else {
246 $languages = $this->languages;
247 }
248
249 // Load translations
250 $mo = array();
251 foreach ( $languages as $language ) {
252 $mo[ $language->slug ] = new PLL_MO();
253 $mo[ $language->slug ]->import_from_db( $language );
254 }
255
256 $data = $this->strings;
257
258 // Filter by selected group
259 if ( -1 !== $this->selected_group ) {
260 $data = wp_list_filter( $data, array( 'context' => $this->selected_group ) );
261 }
262
263 // Filter by searched string
264 $s = empty( $_GET['s'] ) ? '' : wp_unslash( $_GET['s'] ); // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
265
266 if ( ! empty( $s ) ) {
267 // Search in translations
268 $in_translations = $this->search_in_translations( $mo, $s );
269
270 foreach ( $data as $key => $row ) {
271 if ( stripos( $row['name'], $s ) === false && stripos( $row['string'], $s ) === false && ! in_array( $row['string'], $in_translations ) ) {
272 unset( $data[ $key ] );
273 }
274 }
275 }
276
277 // Sorting
278 uasort( $data, array( $this, 'usort_reorder' ) );
279
280 // Paging
281 $per_page = $this->get_items_per_page( 'pll_strings_per_page' );
282 $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() );
283
284 $total_items = count( $data );
285 $this->items = array_slice( $data, ( $this->get_pagenum() - 1 ) * $per_page, $per_page, true );
286
287 $this->set_pagination_args(
288 array(
289 'total_items' => $total_items,
290 'per_page' => $per_page,
291 'total_pages' => ceil( $total_items / $per_page ),
292 )
293 );
294
295 // Translate strings
296 // Kept for the end as it is a slow process
297 foreach ( $languages as $language ) {
298 foreach ( $this->items as $key => $row ) {
299 $this->items[ $key ]['translations'][ $language->slug ] = $mo[ $language->slug ]->translate( $row['string'] );
300 $this->items[ $key ]['row'] = $key; // Store the row number for convenience
301 }
302 }
303 }
304
305 /**
306 * Get the list of possible bulk actions.
307 *
308 * @since 1.1
309 *
310 * @return string[] Array of bulk actions.
311 */
312 public function get_bulk_actions() {
313 return array( 'delete' => __( 'Delete', 'polylang' ) );
314 }
315
316 /**
317 * Get the current action selected from the bulk actions dropdown.
318 * overrides parent function to avoid submit button to trigger bulk actions
319 *
320 * @since 1.8
321 *
322 * @return string|false The action name or False if no action was selected
323 */
324 public function current_action() {
325 return empty( $_POST['submit'] ) ? parent::current_action() : false; // phpcs:ignore WordPress.Security.NonceVerification
326 }
327
328 /**
329 * Displays the dropdown list to filter strings per group
330 *
331 * @since 1.1
332 *
333 * @param string $which only 'top' is supported
334 * @return void
335 */
336 public function extra_tablenav( $which ) {
337 if ( 'top' !== $which ) {
338 return;
339 }
340
341 echo '<div class="alignleft actions">';
342 printf(
343 '<label class="screen-reader-text" for="select-group" >%s</label>',
344 /* translators: accessibility text */
345 esc_html__( 'Filter by group', 'polylang' )
346 );
347 echo '<select id="select-group" name="group">' . "\n";
348 printf(
349 '<option value="-1"%s>%s</option>' . "\n",
350 selected( $this->selected_group, -1, false ),
351 esc_html__( 'View all groups', 'polylang' )
352 );
353
354 foreach ( $this->groups as $group ) {
355 printf(
356 '<option value="%s"%s>%s</option>' . "\n",
357 esc_attr( urlencode( $group ) ),
358 selected( $this->selected_group, $group, false ),
359 esc_html( $group )
360 );
361 }
362 echo '</select>' . "\n";
363
364 submit_button( __( 'Filter', 'polylang' ), 'button', 'filter_action', false, array( 'id' => 'post-query-submit' ) );
365 echo '</div>';
366 }
367
368 /**
369 * Saves the strings translations in DB
370 * Optionaly clean the DB
371 *
372 * @since 1.9
373 *
374 * @return void
375 */
376 public function save_translations() {
377 check_admin_referer( 'string-translation', '_wpnonce_string-translation' );
378
379 if ( ! empty( $_POST['submit'] ) ) {
380 foreach ( $this->languages as $language ) {
381 if ( empty( $_POST['translation'][ $language->slug ] ) || ! is_array( $_POST['translation'][ $language->slug ] ) ) { // In case the language filter is active ( thanks to John P. Bloch )
382 continue;
383 }
384
385 $mo = new PLL_MO();
386 $mo->import_from_db( $language );
387
388 $translations = array_map( 'trim', wp_unslash( $_POST['translation'][ $language->slug ] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
389 foreach ( $translations as $key => $translation ) {
390 /**
391 * Filter the string translation before it is saved in DB
392 * Allows to sanitize strings registered with pll_register_string
393 *
394 * @since 1.6
395 * @since 2.7 The translation passed to the filter is unslashed.
396 *
397 * @param string $translation The string translation.
398 * @param string $name The name as defined in pll_register_string.
399 * @param string $context The context as defined in pll_register_string.
400 */
401 $translation = apply_filters( 'pll_sanitize_string_translation', $translation, $this->strings[ $key ]['name'], $this->strings[ $key ]['context'] );
402 $mo->add_entry( $mo->make_entry( $this->strings[ $key ]['string'], $translation ) );
403 }
404
405 // Clean database ( removes all strings which were registered some day but are no more )
406 if ( ! empty( $_POST['clean'] ) ) {
407 $new_mo = new PLL_MO();
408
409 foreach ( $this->strings as $string ) {
410 $new_mo->add_entry( $mo->make_entry( $string['string'], $mo->translate( $string['string'] ) ) );
411 }
412 }
413
414 isset( $new_mo ) ? $new_mo->export_to_db( $language ) : $mo->export_to_db( $language );
415 }
416
417 add_settings_error( 'general', 'pll_strings_translations_updated', __( 'Translations updated.', 'polylang' ), 'updated' );
418
419 /**
420 * Fires after the strings translations are saved in DB
421 *
422 * @since 1.2
423 */
424 do_action( 'pll_save_strings_translations' );
425 }
426
427 // Unregisters strings registered through WPML API
428 if ( $this->current_action() === 'delete' && ! empty( $_POST['strings'] ) && function_exists( 'icl_unregister_string' ) ) {
429 foreach ( array_map( 'sanitize_key', $_POST['strings'] ) as $key ) {
430 icl_unregister_string( $this->strings[ $key ]['context'], $this->strings[ $key ]['name'] );
431 }
432 }
433
434 // To refresh the page ( possible thanks to the $_GET['noheader']=true )
435 $args = array_intersect_key( $_REQUEST, array_flip( array( 's', 'paged', 'group' ) ) );
436 if ( ! empty( $_GET['paged'] ) && ! empty( $_POST['submit'] ) ) {
437 $args['paged'] = (int) $_GET['paged']; // Don't rely on $_REQUEST['paged'] or $_POST['paged']. See #14
438 }
439 if ( ! empty( $args['s'] ) ) {
440 $args['s'] = urlencode( $args['s'] ); // Searched string needs to be encoded as it comes from $_POST
441 }
442 PLL_Settings::redirect( $args );
443 }
444 }
445