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

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