PluginProbe
Polylang / 2.9.1
Polylang v2.9.1
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 2.9.1, at settings/table-string.php

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