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

408 lines 12.0 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 foreach ( $languages as $language ) {
220 $mo[ $language->slug ] = new PLL_MO();
221 $mo[ $language->slug ]->import_from_db( $language );
222 }
223
224 $data = $this->strings;
225
226 // Filter by selected group
227 if ( -1 !== $this->selected_group ) {
228 $data = wp_list_filter( $data, array( 'context' => $this->selected_group ) );
229 }
230
231 // Filter by searched string
232 $s = empty( $_GET['s'] ) ? '' : wp_unslash( $_GET['s'] ); // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
233
234 if ( ! empty( $s ) ) {
235 // Search in translations
236 $in_translations = $this->search_in_translations( $mo, $s );
237
238 foreach ( $data as $key => $row ) {
239 if ( stripos( $row['name'], $s ) === false && stripos( $row['string'], $s ) === false && ! in_array( $row['string'], $in_translations ) ) {
240 unset( $data[ $key ] );
241 }
242 }
243 }
244
245 // Sorting
246 uasort( $data, array( $this, 'usort_reorder' ) );
247
248 // Paging
249 $per_page = $this->get_items_per_page( 'pll_strings_per_page' );
250 $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() );
251
252 $total_items = count( $data );
253 $this->items = array_slice( $data, ( $this->get_pagenum() - 1 ) * $per_page, $per_page );
254
255 $this->set_pagination_args(
256 array(
257 'total_items' => $total_items,
258 'per_page' => $per_page,
259 'total_pages' => ceil( $total_items / $per_page ),
260 )
261 );
262
263 // Translate strings
264 // Kept for the end as it is a slow process
265 foreach ( $languages as $language ) {
266 foreach ( $this->items as $key => $row ) {
267 $this->items[ $key ]['translations'][ $language->slug ] = $mo[ $language->slug ]->translate( $row['string'] );
268 $this->items[ $key ]['row'] = $key; // Store the row number for convenience
269 }
270 }
271 }
272
273 /**
274 * Get the list of possible bulk actions
275 *
276 * @since 1.1
277 *
278 * @return array
279 */
280 public function get_bulk_actions() {
281 return array( 'delete' => __( 'Delete', 'polylang' ) );
282 }
283
284 /**
285 * Get the current action selected from the bulk actions dropdown.
286 * overrides parent function to avoid submit button to trigger bulk actions
287 *
288 * @since 1.8
289 *
290 * @return string|false The action name or False if no action was selected
291 */
292 public function current_action() {
293 return empty( $_POST['submit'] ) ? parent::current_action() : false; // phpcs:ignore WordPress.Security.NonceVerification
294 }
295
296 /**
297 * Displays the dropdown list to filter strings per group
298 *
299 * @since 1.1
300 *
301 * @param string $which only 'top' is supported
302 */
303 public function extra_tablenav( $which ) {
304 if ( 'top' !== $which ) {
305 return;
306 }
307
308 echo '<div class="alignleft actions">';
309 printf(
310 '<label class="screen-reader-text" for="select-group" >%s</label>',
311 /* translators: accessibility text */
312 esc_html__( 'Filter by group', 'polylang' )
313 );
314 echo '<select id="select-group" name="group">' . "\n";
315 printf(
316 '<option value="-1"%s>%s</option>' . "\n",
317 selected( $this->group_selected, -1, false ),
318 esc_html__( 'View all groups', 'polylang' )
319 );
320
321 foreach ( $this->groups as $group ) {
322 printf(
323 '<option value="%s"%s>%s</option>' . "\n",
324 esc_attr( urlencode( $group ) ),
325 selected( $this->selected_group, $group, false ),
326 esc_html( $group )
327 );
328 }
329 echo '</select>' . "\n";
330
331 submit_button( __( 'Filter', 'polylang' ), 'button', 'filter_action', false, array( 'id' => 'post-query-submit' ) );
332 echo '</div>';
333 }
334
335 /**
336 * Saves the strings translations in DB
337 * Optionaly clean the DB
338 *
339 * @since 1.9
340 */
341 public function save_translations() {
342 check_admin_referer( 'string-translation', '_wpnonce_string-translation' );
343
344 if ( ! empty( $_POST['submit'] ) ) {
345 foreach ( $this->languages as $language ) {
346 if ( empty( $_POST['translation'][ $language->slug ] ) ) { // In case the language filter is active ( thanks to John P. Bloch )
347 continue;
348 }
349
350 $mo = new PLL_MO();
351 $mo->import_from_db( $language );
352
353 foreach ( $_POST['translation'][ $language->slug ] as $key => $translation ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
354 /**
355 * Filter the string translation before it is saved in DB
356 * Allows to sanitize strings registered with pll_register_string
357 *
358 * @since 1.6
359 *
360 * @param string $translation the string translation
361 * @param string $name the name as defined in pll_register_string
362 * @param string $context the context as defined in pll_register_string
363 */
364 $translation = apply_filters( 'pll_sanitize_string_translation', $translation, $this->strings[ $key ]['name'], $this->strings[ $key ]['context'] );
365 $mo->add_entry( $mo->make_entry( $this->strings[ $key ]['string'], $translation ) );
366 }
367
368 // Clean database ( removes all strings which were registered some day but are no more )
369 if ( ! empty( $_POST['clean'] ) ) {
370 $new_mo = new PLL_MO();
371
372 foreach ( $this->strings as $string ) {
373 $new_mo->add_entry( $mo->make_entry( $string['string'], $mo->translate( $string['string'] ) ) );
374 }
375 }
376
377 isset( $new_mo ) ? $new_mo->export_to_db( $language ) : $mo->export_to_db( $language );
378 }
379
380 add_settings_error( 'general', 'pll_strings_translations_updated', __( 'Translations updated.', 'polylang' ), 'updated' );
381
382 /**
383 * Fires after the strings translations are saved in DB
384 *
385 * @since 1.2
386 */
387 do_action( 'pll_save_strings_translations' );
388 }
389
390 // Unregisters strings registered through WPML API
391 if ( $this->current_action() === 'delete' && ! empty( $_POST['strings'] ) && function_exists( 'icl_unregister_string' ) ) {
392 foreach ( array_map( 'sanitize_key', $_POST['strings'] ) as $key ) {
393 icl_unregister_string( $this->strings[ $key ]['context'], $this->strings[ $key ]['name'] );
394 }
395 }
396
397 // To refresh the page ( possible thanks to the $_GET['noheader']=true )
398 $args = array_intersect_key( $_REQUEST, array_flip( array( 's', 'paged', 'group' ) ) );
399 if ( ! empty( $_GET['paged'] ) && ! empty( $_POST['submit'] ) ) {
400 $args['paged'] = (int) $_GET['paged']; // Don't rely on $_REQUEST['paged'] or $_POST['paged']. See #14
401 }
402 if ( ! empty( $args['s'] ) ) {
403 $args['s'] = urlencode( $args['s'] ); // Searched string needs to be encoded as it comes from $_POST
404 }
405 PLL_Settings::redirect( $args );
406 }
407 }
408