├── .gitignore ├── CHANGELOG.md ├── README.md ├── composer.json ├── composer.lock ├── inc ├── class-mltools-custom-fields-translation.php ├── class-mltools-elementor-config-generator.php ├── class-mltools-gutenberg-config-generator.php ├── class-mltools-shortcode-attribute-filter.php ├── class-mltools-shortcode-config.php ├── class-mltools-shortcode-wpml-config-parser.php ├── class-mltools-xml-helper.php ├── wpml-compatibility-test-tools-base.class.php ├── wpml-compatibility-test-tools-messages.class.php ├── wpml-compatibility-test-tools.class.php ├── wpml-compatibility-test-tools.functions.php └── wpml-modify-duplicate-strings.class.php ├── menus └── settings │ ├── auto-translate-duplicate.php │ ├── auto-translate-strings.php │ ├── custom-fields-translation.php │ ├── generator.php │ ├── overview.php │ ├── settings.php │ └── shortcode-helper.php ├── multilingual-tools.php └── res ├── css └── wctt-style.css ├── img ├── spinner.gif ├── wctt-icon.png └── wctt-multiselect-bg.png └── js └── mt-script.js /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.2.6 2 | 3 | ## Improvements 4 | 5 | - Added XML config generator for Gutenberg 6 | 7 | 8 | # 2.2.5 9 | 10 | ## Bugfix 11 | 12 | - Fixed fatal error when Elementor widget settings are empty 13 | 14 | # 2.2.4 15 | 16 | ## Improvements 17 | 18 | - Added XML config generator for Elementor 19 | 20 | # 2.2.3 21 | 22 | ## Improvements 23 | 24 | - Update jQuery changes. 25 | 26 | # 2.2.2 27 | 28 | ## Improvements 29 | 30 | - Update deprecated filter. 31 | 32 | # 2.2.1 33 | 34 | ## Improvements 35 | 36 | - Added strings and contexts count. 37 | 38 | # 2.2.0 39 | 40 | ## Improvements 41 | 42 | - Added order options by name. 43 | - Added support for "display-as-translated" 44 | 45 | ## Bugfix 46 | 47 | - Fixed auto translate strings issue with large number of strings. 48 | 49 | # 2.1.0 50 | 51 | ## Features 52 | 53 | - Add shortcode helper. 54 | 55 | # 2.0.0 56 | 57 | ## Improvements 58 | 59 | - Add a Overview screen which displays loaded configuration and validation errors. 60 | 61 | # 1.4.0 62 | 63 | ## Improvements 64 | 65 | - Add "Copy once" option for custom fields. 66 | 67 | # 1.3.1 68 | 69 | ## Bugfix 70 | 71 | - Fix attribute parent node. 72 | 73 | # 1.3.0 74 | 75 | ## Features 76 | 77 | - Add option for generating shortcodes. 78 | 79 | ## Improvements 80 | 81 | - Refactor XML generator code. 82 | 83 | # 1.2.3 84 | 85 | ## Bugfix 86 | 87 | - Fix "Translation management" notice 88 | 89 | # 1.2.2 90 | 91 | ## Improvements 92 | 93 | - Update composer file 94 | 95 | # 1.2.1 96 | 97 | ## Improvements 98 | 99 | - Add composer file. 100 | - Changelog style updated. 101 | - Minor content duplication code refactoring 102 | 103 | ## Bugfix 104 | 105 | - Fix automatic child selection in generator. 106 | - Fix JS object parsing. 107 | - Fix maximum execution time fatal error. 108 | - Fix duplicate post content special chars conversion. 109 | - Remove alt and title for images in content option. 110 | 111 | # 1.2 112 | 113 | ## Improvements 114 | 115 | - Major code improvements. 116 | - Add notice for successfully saved file in theme folder. 117 | - Add responsive layout. 118 | - Add dropdown for context selection. 119 | - Add dropdown option count. 120 | - Add flags for active languages. 121 | - Add new icon. 122 | 123 | # 1.1.2 124 | 125 | ## Improvements 126 | 127 | - Add checkboxes instead of option list when selecting contexts for translation. 128 | - Add "Toggle All" for admin text checkboxes. 129 | - Some code style improvements. 130 | - Checkboxes are now automatically checked on radio button change. 131 | 132 | ## Bugfix 133 | 134 | - WP Options with 'autoload' set as 'no' were not listed on configuration generator screen. 135 | - Escaped special characters in mt-script.js 136 | - Remove notice, checked strings inputs are remembered. 137 | 138 | # 1.1.1 139 | 140 | ## Improvements 141 | 142 | - Add necessary sanitization & validation. 143 | 144 | # 1.1 145 | 146 | ## Features 147 | 148 | - Add option for generating wpml-config.xml files. 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multilingual Tools 2 | 3 | ![Latest Stable Version](https://img.shields.io/badge/stable-2.2.3-green.svg?style=flat-squar) 4 | ![License](https://img.shields.io/badge/license-GPLv2-red.svg?style=flat-squar) 5 | 6 | Multilingual Tools are set of tools related to [WPML](https://wpml.org) plugin bundle. Created with tendency to ease multilingual testing process, and help out WordPress developers who applied to [GoGlobal Program](https://wpml.org/documentation/theme-compatibility/go-global-program/). 7 | 8 | ## Key features 9 | 10 | - Auto generate strings for translations 11 | - Add language information to post duplicate 12 | - Generate WPML config file ( wpml-config.xml ) 13 | - Assist with Custom Fields translation preferences 14 | 15 | ## Requirements 16 | 17 | For this plugin to work you will need: 18 | 19 | - WPML Multilingual CMS 20 | - WPML String Translation 21 | 22 | All plugins can be downloaded from [WPML.org](https://wpml.org) 23 | 24 | ## Contribute 25 | 26 | If you spot any bug or have idea for useful feature feel free to contribute via [GitHub](https://github.com/OnTheGoSystems/multilingual-tools). 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "otgs/multilingual-tools", 3 | "type": "wordpress-plugin", 4 | "description": "Set of tools related to WPML plugin bundle. Created with tendency to ease WPML compatibility testing process.", 5 | "homepage": "https://wpml.org/documentation/related-projects/wpml-compatibility-test-tools-plugin/", 6 | "license": "GPL-2.0", 7 | "require": { 8 | "php": ">=5.6" 9 | } 10 | } -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "b40cca925aed46cf527bdd48ba810e71", 8 | "packages": [], 9 | "packages-dev": [], 10 | "aliases": [], 11 | "minimum-stability": "stable", 12 | "stability-flags": [], 13 | "prefer-stable": false, 14 | "prefer-lowest": false, 15 | "platform": { 16 | "php": ">=5.6" 17 | }, 18 | "platform-dev": [], 19 | "plugin-api-version": "1.1.0" 20 | } 21 | -------------------------------------------------------------------------------- /inc/class-mltools-custom-fields-translation.php: -------------------------------------------------------------------------------- 1 | get_results( "SELECT DISTINCT meta_key FROM $wpdb->postmeta WHERE meta_key NOT LIKE '\_%' ORDER BY meta_key ASC" ); 14 | 15 | $custom_fields = array(); 16 | 17 | foreach ( $meta_keys as $meta_key ) { 18 | $custom_fields[] = $meta_key->meta_key; 19 | } 20 | 21 | // We need to exclude the fields with defined translation preference in WPML 22 | 23 | $excluded_custom_fields = array(); 24 | 25 | $settings = get_option( 'icl_sitepress_settings' ); 26 | 27 | if ( ! empty( $settings['translation-management']['custom_fields_translation'] ) ) { 28 | foreach ( $settings['translation-management']['custom_fields_translation'] as $custom_field => $value ) { 29 | $excluded_custom_fields[] = $custom_field; 30 | } 31 | } 32 | 33 | // Providing a filter to add more fields to be excluded 34 | 35 | /** 36 | * Example 37 | * 38 | * function my_custom_excluded_fields($excluded_fields) { 39 | * $excluded_fields[] = 'my_custom_field_1'; 40 | * $excluded_fields[] = 'my_custom_field_2'; 41 | * return $excluded_fields; 42 | * } 43 | * add_filter('wpml_custom_fields_helper_excluded_custom_fields', 'my_custom_excluded_fields'); 44 | */ 45 | 46 | $excluded_custom_fields = apply_filters( 'wpml_custom_fields_helper_excluded_custom_fields', $excluded_custom_fields ); 47 | 48 | $custom_fields = array_diff( $custom_fields, $excluded_custom_fields ); 49 | 50 | // We don't need these fields wpml_, attribute_pa-, acfml, etc.. 51 | 52 | $excluded_prefixes = [ 'acfml', 'attribute_pa', 'wpml', 'wpform' ]; 53 | 54 | $custom_fields = array_filter( $custom_fields, function ( $field ) use ( $excluded_prefixes ) { 55 | foreach ( $excluded_prefixes as $prefix ) { 56 | if ( strpos( $field, $prefix ) === 0 ) { 57 | return false; 58 | } 59 | } 60 | 61 | return true; 62 | } ); 63 | 64 | 65 | return $custom_fields; 66 | } 67 | 68 | public function determine_translation_preference() { 69 | 70 | global $wpdb; 71 | 72 | $custom_fields = $this->get_custom_fields(); 73 | $translation_preferences = array(); 74 | 75 | foreach ( $custom_fields as $custom_field ) { 76 | 77 | $value = $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = %s LIMIT 1", $custom_field ) ); 78 | 79 | // Check if value is numeric, a date string, or specific strings 80 | 81 | if ( $value ) { 82 | $date = DateTime::createFromFormat( 'd-m-Y', $value ); 83 | $date_errors = DateTime::getLastErrors(); 84 | } 85 | 86 | // These values should be copied to translations 87 | $copy_values = [ 'yes', 'no', 'on', 'off', 'true', 'false', 'default' ]; 88 | 89 | // Is it a hash-like string? Something like ffd4rf34d should be set to copy 90 | $isHashString = $value && strlen( $value ) > 5 && preg_match( '/\d/', $value ) && preg_match( '/[a-zA-Z]/', $value ) && strpos( $value, ' ' ) === false; 91 | 92 | 93 | if ( is_numeric( $value ) || 94 | ( $date && $date_errors['warning_count'] == 0 && $date_errors['error_count'] == 0 ) || 95 | in_array( $value, $copy_values ) || 96 | is_serialized( $value ) || 97 | null || 98 | empty( $value ) || 99 | // Check if the value is an email or a URL. 100 | filter_var( $value, FILTER_VALIDATE_EMAIL ) || 101 | strpos( $value, 'http' ) !== false 102 | || $isHashString 103 | ) { 104 | $translation_preferences[ $custom_field ] = 'copy'; 105 | } else { 106 | $translation_preferences[ $custom_field ] = 'translate'; 107 | } 108 | } 109 | 110 | return $translation_preferences; 111 | } 112 | 113 | public function wpml_cf_generate_xml() { 114 | 115 | check_ajax_referer( 'wpml_cf_nonce', 'wpml_cf_nonce' ); 116 | 117 | // Prepare the base of your XML 118 | $wpml_config = ''; 119 | foreach ( $_POST['cf'] as $custom_field => $preference ) { 120 | $custom_field = sanitize_text_field( $custom_field ); 121 | $preference = sanitize_text_field( $preference ); 122 | 123 | $wpml_config .= "$custom_field"; 124 | } 125 | 126 | $wpml_config .= ''; 127 | 128 | // Create the XML file 129 | $formatted_xml = $this->format_xml( $wpml_config ); 130 | 131 | echo $formatted_xml; 132 | 133 | wp_die(); 134 | } 135 | 136 | 137 | public function format_xml( $xml_string ) { 138 | $dom = new DOMDocument; 139 | $dom->preserveWhiteSpace = false; 140 | $dom->loadXML( $xml_string ); 141 | $dom->formatOutput = true; 142 | 143 | return htmlentities( $dom->saveXML( $dom->documentElement ) ); 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /inc/class-mltools-elementor-config-generator.php: -------------------------------------------------------------------------------- 1 | ID, '_elementor_data', true ); 20 | 21 | if ( ! empty( $elementor_data ) ) { 22 | foreach ( $screens as $screen ) { 23 | add_meta_box( 24 | 'wpmlpb_config_generator_box', 25 | 'WPML - Config Generator for Elementor', 26 | array( $this, 'meta_box_html' ), 27 | $screen 28 | ); 29 | } 30 | } 31 | } 32 | 33 | public function get_widgets_blacklist() { 34 | $widgets_blacklist = array(); 35 | 36 | if ( class_exists( 'WPML_Elementor_Translatable_Nodes' ) ) { 37 | $wpml_elementor_translatable_nodes = new WPML_Elementor_Translatable_Nodes(); 38 | $default_widgets = $wpml_elementor_translatable_nodes->get_nodes_to_translate(); 39 | $default_widgets = apply_filters( 'wpml_elementor_widgets_to_translate', $default_widgets ); 40 | 41 | foreach ( $default_widgets as $key => $value ) { 42 | $widgets_blacklist[] = $key; 43 | } 44 | } 45 | 46 | return $widgets_blacklist; 47 | } 48 | 49 | public function get_settings_blacklist() { 50 | $blacklist = array(); 51 | 52 | $blacklist[] = 'is_external'; 53 | $blacklist[] = 'nofollow'; 54 | $blacklist[] = 'custom_attributes'; 55 | 56 | return $blacklist; 57 | } 58 | 59 | public function get_widgets_list( $elements ) { 60 | $widgets = array(); 61 | 62 | foreach ( $elements as $element ) { 63 | if ( $element->elType === 'widget' && isset( $element->settings ) && is_object( $element->settings ) ) { 64 | $widgetType = $element->widgetType; 65 | 66 | $settings = $element->settings; 67 | 68 | if ( is_object( $settings ) ) { 69 | $settings = (array) get_object_vars( $settings ); 70 | } 71 | 72 | foreach ( $settings as $field_key => $field_value ) { 73 | $settings[ $field_key ] = $this->get_field_from_widget( $field_key, $field_value, $widgetType ); 74 | } 75 | 76 | $widgets[ $widgetType ] = array(); 77 | $widgets[ $widgetType ]['widgetType'] = $widgetType; 78 | $widgets[ $widgetType ]['settings'] = $settings; 79 | } 80 | 81 | if ( ! empty( $element->elements ) ) { 82 | $widgets = array_merge( $widgets, $this->get_widgets_list( $element->elements ) ); 83 | } 84 | } 85 | 86 | return $widgets; 87 | } 88 | 89 | public function get_field_from_widget( $field_key, $field_value, $parent = '' ) { 90 | // Repeater Fields 91 | if ( is_array( $field_value ) ) { 92 | $field['fieldKey'] = $field_key; 93 | $field['fieldType'] = 'repeater_field'; 94 | $field['parent'] = $parent; 95 | $field['subFields'] = array(); 96 | 97 | if ( is_array( $field_value ) && array_key_exists( '0', $field_value ) ) { 98 | $field_value = $field_value['0']; 99 | } 100 | 101 | foreach ( $field_value as $subfield_key => $subfield_value ) { 102 | $field['subFields'][ $subfield_key ] = $this->get_field_from_widget( $subfield_key, $subfield_value, $field_key ); 103 | } 104 | } 105 | 106 | // Composite Fields 107 | elseif ( is_object( $field_value ) ) { 108 | $field['fieldKey'] = $field_key; 109 | $field['fieldType'] = 'parent_field'; 110 | $field['parent'] = $parent; 111 | $field['subFields'] = array(); 112 | $field_value = (array) $field_value; 113 | 114 | foreach ( $field_value as $subfield_key => $subfield_value ) { 115 | $field['subFields'][ $subfield_key ] = $this->get_field_from_widget( $subfield_key, $subfield_value, $field_key ); 116 | } 117 | } 118 | 119 | // Regular Fields 120 | else { 121 | $field['fieldKey'] = $field_key; 122 | $field['fieldType'] = 'simple_field'; 123 | $field['parent'] = $parent; 124 | $field['subFields'] = ''; 125 | $field['fieldContent'] = $field_value; 126 | } 127 | 128 | return $field; 129 | } 130 | 131 | public function generate_wpml_config_xml( $widgets ) { 132 | if ( empty( $widgets ) ) { 133 | return __( 'All widgets on this page are already registered.', 'wpml-compatibility-test-tools' ); 134 | } 135 | 136 | $settings_blacklist = $this->get_settings_blacklist(); 137 | $xml = new SimpleXMLElement( '' ); 138 | $elementor_widgets = $xml->addChild( 'elementor-widgets' ); 139 | $registered_widgets = array(); 140 | 141 | foreach ( $widgets as $widget ) { 142 | if ( in_array( $widget['widgetType'], $registered_widgets ) ) { 143 | continue; 144 | } 145 | 146 | $registered_widgets[] = $widget['widgetType']; 147 | $widget_xml = $elementor_widgets->addChild( 'widget' ); 148 | $widget_xml->addAttribute( 'name', $widget['widgetType'] ); 149 | $fields = $widget_xml->addChild( 'fields' ); 150 | 151 | foreach ( $widget['settings'] as $key => $value ) { 152 | if ( in_array( $key, $settings_blacklist ) ) { 153 | continue; 154 | } 155 | 156 | if ( $value['fieldType'] == 'simple_field' ) { 157 | $field = $fields->addChild( 'field', $key ); 158 | } 159 | // Repeater Fields 160 | elseif ( $value['fieldType'] == 'repeater_field' ) { 161 | $field = $fields->addChild( 'field', $key ); 162 | $fields_in_item = $widget_xml->addChild( 'fields-in-item' ); 163 | $fields_in_item->addAttribute( 'items_of', $key ); 164 | 165 | $repeater_registered_keys = array(); 166 | 167 | foreach ( $value['subFields'] as $sub_key => $sub_value ) { 168 | // Parent fields inside repeater fields 169 | if ( $sub_value['fieldType'] == 'parent_field' ) { 170 | $repeater_subfield_registered_keys = array(); 171 | 172 | foreach ( $sub_value['subFields'] as $sub_key_child => $sub_value_child ) { 173 | if ( in_array( $sub_key_child, $settings_blacklist ) || in_array( $sub_key_child, $repeater_subfield_registered_keys ) ) { 174 | continue; 175 | } 176 | 177 | $field_in_item = $fields_in_item->addChild( 'field', $sub_value_child['parent'] . '>' . $sub_key_child ); 178 | $repeater_subfield_registered_keys[] = $sub_key_child; 179 | } 180 | } 181 | // Simple fields inside repeater fields 182 | else { 183 | if ( in_array( $sub_key, $settings_blacklist ) || in_array( $sub_key, $repeater_registered_keys ) ) { 184 | continue; 185 | } 186 | $field_in_item = $fields_in_item->addChild( 'field', $sub_key ); 187 | $repeater_registered_keys[] = $sub_key; 188 | } 189 | } 190 | } 191 | // Parent Field 192 | elseif ( $value['fieldType'] == 'parent_field' ) { 193 | foreach ( $value['subFields'] as $sub_key => $sub_value ) { 194 | if ( in_array( $sub_key, $settings_blacklist ) ) { 195 | continue; 196 | } 197 | $field = $fields->addChild( 'field', $sub_value['parent'] . '>' . $sub_key ); 198 | } 199 | } 200 | } 201 | } 202 | 203 | $dom = dom_import_simplexml( $xml )->ownerDocument; 204 | $dom->formatOutput = true; 205 | 206 | return $dom->saveXML( $dom->documentElement, LIBXML_NOXMLDECL ); 207 | } 208 | 209 | public function generate_xml_for_all( $widgets ) { 210 | return $this->generate_wpml_config_xml( $widgets ); 211 | } 212 | 213 | public function generate_xml_for_missing_widgets( $widgets ) { 214 | $widgets_blacklist = $this->get_widgets_blacklist(); 215 | 216 | foreach ( $widgets as $key => $widget ) { 217 | if ( in_array( $widget['widgetType'], $widgets_blacklist ) ) { 218 | unset( $widgets[ $key ] ); 219 | } 220 | } 221 | 222 | return $this->generate_wpml_config_xml( $widgets ); 223 | } 224 | 225 | public function meta_box_html( $post ) { 226 | // Variables 227 | $elementor_data = get_post_meta( $post->ID, '_elementor_data', true ); 228 | $elementor_data_array = json_decode( $elementor_data ); 229 | $widgets = $this->get_widgets_list( $elementor_data_array ); 230 | 231 | // XML Config only for missing widgets 232 | echo '

' . __( 'WPML: Elementor Widgets', 'wpml-compatibility-test-tools' ) . '

'; 233 | echo '

' . __( 'XML generated for widgets from this page that does not have translation settings.', 'wpml-compatibility-test-tools' ) . '

'; 234 | echo ''; 237 | 238 | // Debug Information 239 | $sections = array( 240 | array( 241 | 'title' => __( 'WPML Config XML (generated for all widgets in the page)', 'wpml-compatibility-test-tools' ), 242 | 'description' => __( 'WARNING: Using this may overwrite existing settings (including default elementor widgets). Please check it before and use with caution.', 'wpml-compatibility-test-tools' ), 243 | 'content' => htmlspecialchars_decode( $this->generate_xml_for_all( $widgets ) ), 244 | ), 245 | array( 246 | 'title' => __( 'RAW value from _elementor_data (JSON)', 'wpml-compatibility-test-tools' ), 247 | 'description' => __( 'This is the raw value stored in the _elementor_data meta field.', 'wpml-compatibility-test-tools' ), 248 | 'content' => print_r( $elementor_data, true ), 249 | ), 250 | array( 251 | 'title' => __( 'Array generated from _elementor_data', 'wpml-compatibility-test-tools' ), 252 | 'description' => __( 'This is the _elementor_data converted into a PHP array.', 'wpml-compatibility-test-tools' ), 253 | 'content' => print_r( $elementor_data_array, true ), 254 | ), 255 | array( 256 | 'title' => __( 'Extracted Widgets from _elementor_data', 'wpml-compatibility-test-tools' ), 257 | 'description' => __( 'These are the widgets that have been extracted from the _elementor_data array.', 'wpml-compatibility-test-tools' ), 258 | 'content' => print_r( $widgets, true ), 259 | ), 260 | ); 261 | 262 | echo '
' . __( 'WPML: Elementor Debug Information', 'wpml-compatibility-test-tools' ) . '
'; 263 | 264 | foreach ( $sections as $section ) { 265 | echo '
'; 266 | echo '' . __( $section['title'], 'wpml-compatibility-test-tools' ) . ''; 267 | echo '

' . __( $section['description'], 'wpml-compatibility-test-tools' ) . '

'; 268 | echo ''; 271 | echo '
'; 272 | } 273 | } 274 | } 275 | 276 | new MLTools_Elementor_Config_Generator(); 277 | -------------------------------------------------------------------------------- /inc/class-mltools-gutenberg-config-generator.php: -------------------------------------------------------------------------------- 1 | ID ); 39 | $xmlContent = $this->generateXmlFromContent( $content ); 40 | 41 | $this->renderMetaBoxHeader(); 42 | $this->renderXmlTextarea( $xmlContent ); 43 | $this->renderDebugInformation( $content ); 44 | } 45 | 46 | /** 47 | * Renders the header for the meta box. 48 | */ 49 | private function renderMetaBoxHeader() { 50 | echo '

' . esc_html__( 'WPML: Gutenberg Blocks', 'wpml-compatibility-test-tools' ) . '

'; 51 | echo '

' . wp_kses_post( __( 'XML automatically generated for the blocks and block attributes from this page. Please review before using it.', 'wpml-compatibility-test-tools' ) ) . '

'; 52 | echo '

' . wp_kses_post( __( 'For instructions on how to use and implement it, please check the following links:', 'wpml-compatibility-test-tools' ) ) . '

'; 53 | echo '

- ' . esc_html__( 'WPML Language Configuration File', 'wpml-compatibility-test-tools' ) . '

'; 54 | echo '

- ' . esc_html__( 'Make Custom Gutenberg Blocks Translatable', 'wpml-compatibility-test-tools' ) . '

'; 55 | } 56 | 57 | /** 58 | * Renders the XML content in a textarea. 59 | * 60 | * @param string $xmlContent The XML content to display. 61 | */ 62 | private function renderXmlTextarea( $xmlContent ) { 63 | echo ''; 66 | } 67 | 68 | /** 69 | * Renders debug information for the Gutenberg blocks. 70 | * 71 | * @param string $content The post content. 72 | */ 73 | private function renderDebugInformation( $content ) { 74 | $sections = $this->getDebugSections( $content ); 75 | 76 | echo '

' . esc_html__( 'Gutenberg Blocks - Debug Information', 'wpml-compatibility-test-tools' ) . '

'; 77 | 78 | foreach ( $sections as $section ) { 79 | $this->renderDebugSection( $section ); 80 | } 81 | } 82 | 83 | /** 84 | * Gets the debug sections for the Gutenberg blocks. 85 | * 86 | * @param string $content The post content. 87 | * 88 | * @return array The debug sections. 89 | */ 90 | private function getDebugSections( $content ) { 91 | return [ 92 | [ 93 | 'title' => esc_html__( 'Post Content', 'wpml-compatibility-test-tools' ), 94 | 'description' => esc_html__( 'RAW content from the post_content column of the current post.', 'wpml-compatibility-test-tools' ), 95 | 'content' => htmlspecialchars_decode( $content ), 96 | ], 97 | [ 98 | 'title' => esc_html__( 'Parse Blocks from post_content', 'wpml-compatibility-test-tools' ), 99 | 'description' => esc_html__( 'Result of parse_blocks() applied to the page content (JSON format).', 'wpml-compatibility-test-tools' ), 100 | 'content' => htmlspecialchars( json_encode( parse_blocks( $content ), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE ) ), 101 | ], 102 | ]; 103 | } 104 | 105 | /** 106 | * Renders a single debug section. 107 | * 108 | * @param array $section The section data. 109 | */ 110 | private function renderDebugSection( $section ) { 111 | $html = '
'; 112 | $html .= '' . esc_html__( $section['title'], 'wpml-compatibility-test-tools' ) . ''; 113 | $html .= '

' . esc_html__( $section['description'], 'wpml-compatibility-test-tools' ) . '

'; 114 | $html .= ''; 117 | $html .= '
'; 118 | 119 | echo $html; 120 | } 121 | 122 | /** 123 | * Returns a list of non-translatable Gutenberg blocks. 124 | * 125 | * @return array 126 | */ 127 | public function getNonTranslatableBlocksList() { 128 | return apply_filters( 'mltools_gutenberg_non_translatable_blocks_list', self::NON_TRANSLATABLE_BLOCKS ); 129 | } 130 | 131 | /** 132 | * Returns a list of wildcard attributes. 133 | * 134 | * @return array 135 | */ 136 | public function getWildcardAttributesList() { 137 | return apply_filters( 'mltools_gutenberg_wildcart_attributes_list', self::WILDCARD_ATTRIBUTES ); 138 | } 139 | 140 | /** 141 | * Parses the content into an array of Gutenberg blocks. 142 | * 143 | * @param string $content The post content. 144 | * 145 | * @return array 146 | */ 147 | public function parseContentToBlocks( $content ) { 148 | return parse_blocks( $content ); 149 | } 150 | 151 | /** 152 | * Filters and returns an array of Gutenberg blocks. 153 | * 154 | * @param string $content The post content. 155 | * 156 | * @return array 157 | */ 158 | public function getFilteredBlocksArray( $content ) { 159 | $blocks = $this->parseContentToBlocks( $content ); 160 | $filteredBlocks = $this->filterBlocksArray( $blocks ); 161 | 162 | return (array) $filteredBlocks; 163 | } 164 | 165 | /** 166 | * Filters the blocks array to remove duplicates and organize blocks. 167 | * 168 | * @param array $blocks The array of blocks. 169 | * @param array $filteredBlocks The array of filtered blocks. 170 | * 171 | * @return array 172 | */ 173 | public function filterBlocksArray( $blocks, $filteredBlocks = [] ) { 174 | 175 | foreach ( $blocks as $block ) { 176 | if ( isset( $block['blockName'] ) ) { 177 | 178 | if ( ! array_key_exists( $block['blockName'], $filteredBlocks ) ) { 179 | $filteredBlocks[ $block['blockName'] ] = $block; 180 | } elseif ( array_key_exists( $block['blockName'], $filteredBlocks ) ) { 181 | $filteredBlocks[ $block['blockName'] ] = array_merge( 182 | $filteredBlocks[ $block['blockName'] ], 183 | $block 184 | ); 185 | } 186 | if ( isset( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) ) { 187 | 188 | $innerBlocks = $this->filterBlocksArray( $block['innerBlocks'], $filteredBlocks ); 189 | 190 | $filteredBlocks = array_merge( 191 | $filteredBlocks, 192 | $innerBlocks 193 | ); 194 | 195 | unset( $filteredBlocks[ $block['blockName'] ]['innerBlocks'] ); 196 | 197 | } 198 | } 199 | } 200 | 201 | return (array) $filteredBlocks; 202 | } 203 | 204 | /** 205 | * Generates XML from the post content. 206 | * 207 | * @param string $content The post content. 208 | * 209 | * @return string 210 | */ 211 | private function generateXmlFromContent( $content ) { 212 | try { 213 | $blocks = $this->getFilteredBlocksArray( $content ); 214 | $nonTranslatableBlocks = $this->getNonTranslatableBlocksList(); 215 | 216 | $xml = new SimpleXMLElement( '' ); 217 | $gutenbergBlocks = $xml->addChild( 'gutenberg-blocks' ); 218 | 219 | foreach ( $blocks as $block ) { 220 | $this->generateXmlForBlock( $gutenbergBlocks, $block, $nonTranslatableBlocks ); 221 | } 222 | 223 | $dom = dom_import_simplexml( $xml )->ownerDocument; 224 | $dom->formatOutput = true; 225 | return $dom->saveXML( $dom->documentElement, LIBXML_NOXMLDECL ); 226 | } catch ( Exception $e ) { 227 | error_log( 'Error generating XML: ' . $e->getMessage() ); 228 | return ''; 229 | } 230 | } 231 | 232 | /** 233 | * Generates XML for a single Gutenberg block. 234 | * 235 | * @param SimpleXMLElement $xmlElement The XML element to append to. 236 | * @param array $block The block data. 237 | * @param array $nonTranslatableBlocks The list of non-translatable blocks. 238 | */ 239 | private function generateXmlForBlock( $xmlElement, $block, $nonTranslatableBlocks ) { 240 | if ( ! isset( $block['blockName'] ) || empty( $block['blockName'] ) ) { 241 | return; 242 | } 243 | 244 | $blockType = $block['blockName']; 245 | $translate = in_array( $block['blockName'], $nonTranslatableBlocks ) ? '0' : '1'; 246 | 247 | $blockElement = $xmlElement->addChild( 'gutenberg-block' ); 248 | $blockElement->addAttribute( 'type', $blockType ); 249 | $blockElement->addAttribute( 'translate', $translate ); 250 | 251 | if ( $translate === '1' && ! empty( $block['innerHTML'] ) ) { 252 | $this->addXpathElements( $blockElement, $block ); 253 | } 254 | 255 | if ( ! empty( $block['attrs'] && ! in_array( $block['blockName'], $nonTranslatableBlocks ) ) ) { 256 | $this->generateXmlForAttributes( $blockElement, $block['attrs'] ); 257 | } 258 | 259 | if ( ! empty( $block['innerBlocks'] ) ) { 260 | foreach ( $block['innerBlocks'] as $innerBlock ) { 261 | $this->generateXmlForBlock( $xmlElement, $innerBlock, $nonTranslatableBlocks ); 262 | } 263 | } 264 | } 265 | 266 | /** 267 | * Generates XML for block attributes. 268 | * 269 | * @param SimpleXMLElement $xmlElement The XML element to append to. 270 | * @param array $attrs The block attributes. 271 | */ 272 | private function generateXmlForAttributes( $xmlElement, $attrs ) { 273 | $wildcardAttributesList = $this->getWildcardAttributesList(); 274 | 275 | foreach ( $attrs as $key => $value ) { 276 | if ( is_array( $value ) ) { 277 | if ( in_array( $key, $wildcardAttributesList ) ) { 278 | $key = '*'; 279 | } 280 | $keyElement = $xmlElement->addChild( 'key' ); 281 | $keyElement->addAttribute( 'name', $key ); 282 | $this->generateXmlForAttributes( $keyElement, $value ); 283 | } else { 284 | $keyElement = $xmlElement->addChild( 'key' ); 285 | $keyElement->addAttribute( 'name', $key ); 286 | 287 | if ( strpos( $key, 'url' ) !== false || 288 | strpos( $key, 'link' ) !== false || 289 | strpos( $key, 'href' ) !== false ) { 290 | $keyElement->addAttribute( 'type', 'link' ); 291 | } 292 | } 293 | } 294 | } 295 | 296 | /** 297 | * Adds xpath elements to the block element based on the HTML content. 298 | * 299 | * @param SimpleXMLElement $blockElement The block XML element. 300 | * @param array $block The block data. 301 | */ 302 | private function addXpathElements( $blockElement, $block ) { 303 | if ( empty( $block['innerHTML'] ) ) { 304 | return; 305 | } 306 | 307 | $xpaths = $this->analyzeHtmlForXpaths( $block['innerHTML'] ); 308 | 309 | foreach ( $xpaths as $xpath ) { 310 | $child = $blockElement->addChild( 'xpath', $xpath ); 311 | 312 | if ( strpos( $xpath, '@href' ) !== false ) { 313 | $child->addAttribute( 'type', 'link' ); 314 | } 315 | } 316 | } 317 | 318 | /** 319 | * Analyzes HTML content to determine appropriate xpaths. 320 | * 321 | * @param string $html The HTML content. 322 | * 323 | * @return array Array of xpath expressions. 324 | */ 325 | private function analyzeHtmlForXpaths( $html ) { 326 | $xpaths = []; 327 | 328 | if ( empty( trim( $html ) ) ) { 329 | return $xpaths; 330 | } 331 | 332 | $doc = new DOMDocument(); 333 | @$doc->loadHTML( '
' . $html . '
', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); 334 | 335 | $xpath = new DOMXPath( $doc ); 336 | 337 | $textNodes = $this->findTextNodes( $doc->documentElement ); 338 | 339 | foreach ( $textNodes as $node ) { 340 | if ( $node->nodeType === XML_TEXT_NODE ) { 341 | $text = trim( $node->nodeValue ); 342 | if ( $this->isTranslatableText( $text ) ) { 343 | $parent = $node->parentNode; 344 | if ( $parent !== null ) { 345 | $xpathExpression = $this->generateUniqueXPath( $parent ); 346 | if ( ! empty( $xpathExpression ) ) { 347 | $xpaths[] = $xpathExpression; 348 | } 349 | } 350 | } 351 | } 352 | } 353 | 354 | $links = $xpath->query( '//a[@href]' ); 355 | foreach ( $links as $link ) { 356 | $xpaths[] = $this->generateUniqueXPath( $link ) . '/@href'; 357 | } 358 | 359 | $attrMap = [ 360 | 'img' => [ 'alt', 'title' ], 361 | 'input' => [ 'placeholder', 'value' ], 362 | 'textarea' => [ 'placeholder' ], 363 | 'button' => [ 'value' ], 364 | 'meta' => [ 'content' ], 365 | ]; 366 | 367 | foreach ( $attrMap as $tag => $attributes ) { 368 | $elements = $doc->getElementsByTagName( $tag ); 369 | foreach ( $elements as $element ) { 370 | foreach ( $attributes as $attribute ) { 371 | if ( $element->hasAttribute( $attribute ) && $this->isTranslatableText( $element->getAttribute( $attribute ) ) ) { 372 | $xpaths[] = $this->generateUniqueXPath( $element ) . '/@' . $attribute; 373 | } 374 | } 375 | } 376 | } 377 | 378 | return array_unique( $xpaths ); 379 | } 380 | 381 | /** 382 | * Recursively finds text nodes within an element. 383 | * 384 | * @param DOMNode $node The node to check. 385 | * 386 | * @return array Array of text nodes. 387 | */ 388 | private function findTextNodes( $node ) { 389 | $textNodes = []; 390 | 391 | if ( $node->nodeType === XML_TEXT_NODE ) { 392 | $text = trim( $node->nodeValue ); 393 | if ( $this->isTranslatableText( $text ) ) { 394 | $textNodes[] = $node; 395 | } 396 | } 397 | 398 | if ( $node->hasChildNodes() ) { 399 | foreach ( $node->childNodes as $child ) { 400 | $textNodes = array_merge( $textNodes, $this->findTextNodes( $child ) ); 401 | } 402 | } 403 | 404 | return $textNodes; 405 | } 406 | 407 | /** 408 | * Generates a unique XPath for a DOM element. 409 | * 410 | * @param DOMElement $element The DOM element. 411 | * 412 | * @return string The XPath expression. 413 | */ 414 | private function generateUniqueXPath( $element ) { 415 | if ( $element->nodeType !== XML_ELEMENT_NODE ) { 416 | return ''; 417 | } 418 | 419 | $nodePath = $element->nodeName; 420 | 421 | if ( $element->hasAttribute( 'class' ) ) { 422 | $classes = explode( ' ', $element->getAttribute( 'class' ) ); 423 | $filteredClasses = array_filter( $classes, 'trim' ); 424 | 425 | if ( ! empty( $filteredClasses ) ) { 426 | $selectedClass = $this->selectMostUniqueClass( $filteredClasses ); 427 | $nodePath .= '[contains(@class, "' . $selectedClass . '")]'; 428 | } 429 | } else { 430 | $parent = $element->parentNode; 431 | if ( $parent && $parent->nodeType === XML_ELEMENT_NODE ) { 432 | $siblings = 0; 433 | $position = 0; 434 | 435 | foreach ( $parent->childNodes as $i => $sibling ) { 436 | if ( $sibling->nodeType === XML_ELEMENT_NODE && $sibling->nodeName === $element->nodeName ) { 437 | $siblings++; 438 | if ( $sibling === $element ) { 439 | $position = $siblings; 440 | } 441 | } 442 | } 443 | 444 | if ( $siblings > 1 ) { 445 | $nodePath .= '[' . $position . ']'; 446 | } 447 | } 448 | } 449 | 450 | if ( $element->childNodes->length === 1 && $element->firstChild->nodeType === XML_TEXT_NODE ) { 451 | return '//' . $nodePath; 452 | } 453 | 454 | return '//' . $nodePath; 455 | } 456 | 457 | /** 458 | * Selects the most unique class from a list of classes. 459 | * 460 | * @param array $classes Array of class names. 461 | * 462 | * @return string The selected class name. 463 | */ 464 | private function selectMostUniqueClass( $classes ) { 465 | foreach ( $classes as $class ) { 466 | if ( strpos( $class, 'id' ) !== false || 467 | strpos( $class, 'ID' ) !== false || 468 | preg_match( '/[A-Za-z0-9]{5,}/', $class ) ) { 469 | return $class; 470 | } 471 | } 472 | 473 | return $this->getLongestClass( $classes ); 474 | } 475 | 476 | /** 477 | * Gets the longest class name from a list of classes. 478 | * 479 | * This method sorts the array of class names by length (descending) 480 | * and returns the first (longest) element. This is used as a fallback 481 | * strategy when we can't find classes with specific patterns. 482 | * 483 | * @param array $classes Array of class names to analyze. 484 | * 485 | * @return string The class name with the longest string length or empty string if array is empty. 486 | */ 487 | private function getLongestClass( $classes ) { 488 | if ( empty( $classes ) ) { 489 | return ''; 490 | } 491 | 492 | usort( 493 | $classes, 494 | function( $a, $b ) { 495 | return strlen( $b ) - strlen( $a ); 496 | } 497 | ); 498 | 499 | return reset( $classes ); 500 | } 501 | 502 | /** 503 | * @param string $text The text to check. 504 | * 505 | * @return bool Whether the text is translatable. 506 | */ 507 | private function isTranslatableText( $text ) { 508 | $text = trim( $text ); 509 | return ! empty( $text ) && ! is_numeric( $text ); 510 | } 511 | } 512 | 513 | new MLTools_Gutenberg_Config_Generator(); 514 | -------------------------------------------------------------------------------- /inc/class-mltools-shortcode-attribute-filter.php: -------------------------------------------------------------------------------- 1 | captured_tags = get_option( self::OPTION_NAME, array() ); 14 | $this->captured_values = get_option( SELF::OPTION_NAME_VALUES, array() ); 15 | $this->ignored_tags = $ignored_tags; 16 | } 17 | 18 | public function add_hooks() { 19 | if ( ! is_admin() ) { 20 | add_action( 'wp_head', array( $this, 'add_shortcode_filters' ) ); 21 | add_filter( 'do_shortcode_tag', array( $this, 'do_shortcode_tag_filter' ), 10, 3 ); 22 | add_action( 'shutdown', array( $this, 'save_tags' ) ); 23 | } 24 | } 25 | 26 | public function add_shortcode_filters() { 27 | 28 | global $shortcode_tags; 29 | 30 | foreach ( $shortcode_tags as $tag => $callback ) { 31 | if ( ! in_array( $tag, $this->ignored_tags ) ) { 32 | add_filter( "shortcode_atts_{$tag}", array( $this, 'shortcode_atts_filter' ), 10, 4 ); 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * Shortcode attribute filter. 39 | * 40 | * Notice: shortcode_atts() must be called with 3rd parameter $shortcode, 41 | * otherwise this filter will be not be applied. 42 | * 43 | * Example: 44 | * extract( shortcode_atts( $default_atts, $atts, 'My_Widget' ) ) 45 | * 46 | * @param string $out Output. 47 | * @param array $pairs Default attributes. 48 | * @param array $atts Shortcode attributes. 49 | * @param string $tag Shortcode tag. 50 | * 51 | * @return mixed 52 | */ 53 | public function shortcode_atts_filter( $out, $pairs, $atts, $tag ) { 54 | 55 | if ( is_array( $pairs ) && is_array( $atts ) ) { 56 | $all_attributes = array_merge( $pairs, $atts ); 57 | $this->add_tag( $tag, $all_attributes ); 58 | } 59 | 60 | return $out; 61 | } 62 | 63 | public function do_shortcode_tag_filter( $output, $tag, $attr ) { 64 | 65 | if ( ! in_array( $tag, $this->ignored_tags ) && is_array( $attr ) ) { 66 | $this->add_tag( $tag, $attr ); 67 | } 68 | 69 | return $output; 70 | } 71 | 72 | private function add_tag( $tag, $attributes ) { 73 | 74 | $props = array(); 75 | if ( isset( $this->captured_tags[ $tag ] ) ) { 76 | $props = $this->captured_tags[ $tag ]; 77 | } 78 | 79 | $config = new MLTools_Shortcode_Config( $tag, $props ); 80 | 81 | foreach ( $attributes as $attr_name => $attr_value ) { 82 | $config->add_attribute( $attr_name ); 83 | $this->captured_values[ $tag ]['attributes'][ $attr_name ] = $attr_value; 84 | } 85 | 86 | ksort( $this->captured_values[ $tag ]['attributes'] ); 87 | 88 | $props = $config->get_props(); 89 | ksort( $props['attributes'] ); 90 | 91 | $this->captured_tags[ $tag ] = $props; 92 | } 93 | 94 | public function save_tags() { 95 | update_option( self::OPTION_NAME, $this->get_tags() ); 96 | update_option( self::OPTION_NAME_VALUES, $this->get_captured_values() ); 97 | } 98 | 99 | private function get_tags() { 100 | 101 | foreach ( $this->ignored_tags as $tag ) { 102 | unset( $this->captured_tags[ $tag ] ); 103 | } 104 | 105 | ksort( $this->captured_tags ); 106 | 107 | return $this->captured_tags; 108 | } 109 | 110 | private function get_captured_values() { 111 | 112 | foreach ( $this->ignored_tags as $tag ) { 113 | unset( $this->captured_values[ $tag ] ); 114 | } 115 | 116 | ksort( $this->captured_values ); 117 | 118 | return $this->captured_values; 119 | } 120 | 121 | } -------------------------------------------------------------------------------- /inc/class-mltools-shortcode-config.php: -------------------------------------------------------------------------------- 1 | false, 7 | 'attributes' => array(), 8 | ); 9 | 10 | function __construct( $tag, $props = array() ) { 11 | if ( ! empty( $props ) ) { 12 | foreach ( $props as $prop => $value ) { 13 | $this->props[ $prop ] = $value; 14 | } 15 | } 16 | $this->props['tag'] = $tag; 17 | } 18 | 19 | public function set( $name, $value ) { 20 | if ( isset( $this->props[ $name ] ) ) { 21 | $this->props[ $name ] = $value; 22 | } 23 | } 24 | 25 | public function get_props() { 26 | return $this->props; 27 | } 28 | 29 | public function __get( $name ) { 30 | return isset( $this->props[ $name ] ) ? $this->props[ $name ] : null; 31 | } 32 | 33 | public function __set( $name, $value ) { 34 | user_error( 'Use set' ); 35 | } 36 | 37 | public function add_attribute( $attr_name ) { 38 | if ( ! isset( $this->props['attributes'][ $attr_name ] ) ) { 39 | $this->props['attributes'][ $attr_name ] = array(); 40 | } 41 | } 42 | 43 | public function set_attribute_property( $attr_name, $prop, $value ) { 44 | if ( isset( $this->props['attributes'][ $attr_name ] ) ) { 45 | $this->props['attributes'][ $attr_name ][ $prop ] = $value; 46 | } 47 | } 48 | 49 | public function get_attribute_property( $attr_name, $prop ) { 50 | return isset( $this->props['attributes'][ $attr_name ][ $prop ] ) ? $this->props['attributes'][ $attr_name ][ $prop ] : null; 51 | } 52 | } -------------------------------------------------------------------------------- /inc/class-mltools-shortcode-wpml-config-parser.php: -------------------------------------------------------------------------------- 1 | set( 'encoding', $shortcode['tag']['attr']['encoding'] ); 34 | } 35 | 36 | if ( isset( $shortcode['tag']['attr']['type'] ) ) { 37 | $config->set( 'type', $shortcode['tag']['attr']['type'] ); 38 | } 39 | 40 | if ( isset( $shortcode['attributes']['attribute'] ) && is_array( $shortcode['attributes']['attribute'] ) ) { 41 | 42 | if ( isset( $shortcode['attributes']['attribute']['value'] ) ) { 43 | 44 | $attr_name = $shortcode['attributes']['attribute']['value']; 45 | $config->add_attribute( $attr_name ); 46 | 47 | if ( isset( $shortcode['attributes']['attribute']['attr']['encoding'] ) ) { 48 | $config->set_attribute_property( $attr_name, 'encoding', $shortcode['attributes']['attribute']['attr']['encoding'] ); 49 | } 50 | 51 | if ( isset( $shortcode['attributes']['attribute']['attr']['type'] ) ) { 52 | $config->set_attribute_property( $attr_name, 'type', $shortcode['attributes']['attribute']['attr']['type'] ); 53 | } 54 | } else { 55 | foreach ( $shortcode['attributes']['attribute'] as $attr ) { 56 | 57 | $attr_name = $attr['value']; 58 | $config->add_attribute( $attr_name ); 59 | 60 | if ( isset( $attr['attr']['encoding'] ) ) { 61 | $config->set_attribute_property( $attr_name, 'encoding', $attr['attr']['encoding'] ); 62 | } 63 | 64 | if ( isset( $attr['attr']['type'] ) ) { 65 | $config->set_attribute_property( $attr_name, 'type', $attr['attr']['type'] ); 66 | } 67 | } 68 | } 69 | } 70 | $wpml_shortcodes[ $tag ] = $config->get_props(); 71 | } 72 | } 73 | 74 | return $wpml_shortcodes; 75 | } 76 | 77 | public static function get_config() { 78 | 79 | if ( self::$config === null ) { 80 | 81 | // @todo Fix dependencies 82 | $array_utility_file = WPML_PLUGIN_PATH . '/inc/utilities/xml2array.php'; 83 | 84 | if ( file_exists( $array_utility_file ) ) { 85 | require_once $array_utility_file; 86 | WPML_Config::load_config_run(); 87 | } else { 88 | return false; 89 | } 90 | } 91 | 92 | return self::$config; 93 | } 94 | 95 | } -------------------------------------------------------------------------------- /inc/class-mltools-xml-helper.php: -------------------------------------------------------------------------------- 1 | preserveWhiteSpace = false; 9 | $xml->formatOutput = true; 10 | $xml_shortcodes = $xml->createElement( 'shortcodes' ); 11 | foreach ( $shortcodes as $tag => $config ) { 12 | $xml_shortcodes->appendChild( $this->append_shortcode( $config, $xml ) ); 13 | } 14 | $xml->appendChild( $xml_shortcodes ); 15 | 16 | return $xml->saveXML(); 17 | } 18 | 19 | public function get_dom_single_shortcode( array $config ) { 20 | 21 | $xml = new DOMDocument( "1.0", "UTF-8" ); 22 | $xml->preserveWhiteSpace = false; 23 | $xml->formatOutput = true; 24 | $xml->appendChild( $this->append_shortcode( $config, $xml ) ); 25 | 26 | return $xml->saveXML(); 27 | } 28 | 29 | private function append_shortcode( array $config, DOMDocument $xml ) { 30 | 31 | $xml_shortcode = $xml->createElement( 'shortcode' ); 32 | $xml_tag = $xml->createElement( 'tag', $config['tag'] ); 33 | $xml_shortcode->appendChild( $xml_tag ); 34 | 35 | $attributes = $config['attributes']; 36 | 37 | if ( ! empty( $attributes ) ) { 38 | $xml_attributes = $xml->createElement( 'attributes' ); 39 | foreach ( $attributes as $attr_name => $props ) { 40 | $xml_attribute = $xml->createElement( 'attribute', $attr_name ); 41 | foreach ( $props as $prop_name => $prop_value ) { 42 | $xml_attribute->setAttribute( $prop_name, $prop_value ); 43 | } 44 | $xml_attributes->appendChild( $xml_attribute ); 45 | } 46 | $xml_shortcode->appendChild( $xml_attributes ); 47 | } 48 | 49 | return $xml_shortcode; 50 | } 51 | } -------------------------------------------------------------------------------- /inc/wpml-compatibility-test-tools-base.class.php: -------------------------------------------------------------------------------- 1 | messages = new WPML_Compatibility_Test_Tools_Messages(); 11 | } 12 | 13 | /** 14 | * Save initial configuration to database 15 | */ 16 | public static function install() { 17 | if ( get_option( self::OPTIONS_NAME ) === false ) { 18 | $options[ 'string_auto_translate_template' ] = '[%language_name%] %original_string%'; 19 | $options[ 'duplicate_strings_template' ] = '[%language_name%] %original_string%'; 20 | $options[ 'shortcode_enable_debug' ] = false; 21 | $options[ 'shortcode_enable_debug_value' ] = false; 22 | $options[ 'shortcode_ignored_tags' ] = false; 23 | 24 | add_option( self::OPTIONS_NAME, $options ); 25 | } 26 | 27 | return true; 28 | } 29 | 30 | /** 31 | * Return plugin option 32 | * 33 | * @param $option_name 34 | * @param null $default 35 | * 36 | * @return null 37 | */ 38 | public static function get_option( $option_name, $default = null ) { 39 | if ( empty( self::$options ) ) { 40 | self::$options = get_option( self::OPTIONS_NAME ); 41 | } 42 | 43 | if ( isset( self::$options[$option_name] ) ) { 44 | return self::$options[$option_name]; 45 | } 46 | 47 | return $default; 48 | } 49 | 50 | /** 51 | * Update plugin option 52 | * 53 | * @param $option_name 54 | * @param $option_value 55 | * @return bool 56 | */ 57 | public static function update_option($option_name, $option_value ) { 58 | $options = get_option( self::OPTIONS_NAME ); 59 | $options[$option_name] = $option_value; 60 | $result = update_option( self::OPTIONS_NAME, $options ); 61 | 62 | if ( $result ) { 63 | self::refresh_options(); 64 | } 65 | 66 | return $result; 67 | } 68 | 69 | /** 70 | * Refresh options 71 | */ 72 | public static function refresh_options() { 73 | self::$options = get_option( self::OPTIONS_NAME ); 74 | } 75 | } -------------------------------------------------------------------------------- /inc/wpml-compatibility-test-tools-messages.class.php: -------------------------------------------------------------------------------- 1 |

' . sprintf( $message, 'WPML' ) . '

'; 10 | break; 11 | 12 | case 'not_finished_wpml_setup' : 13 | $message = __( 'Multilingual Tools plugin is enabled but not effective. You have to finish WPML setup.', 'wpml-compatibility-test-tools' ); 14 | echo '

' . sprintf( $message, 'WPML' ) . '

'; 15 | break; 16 | 17 | case 'no_tm_notice' : 18 | $message = __( 'Multilingual Tools plugin is enabled but not effective. It requires WPML Translation Management plugin in order to work.', 'wpml-compatibility-test-tools' ); 19 | echo '

' . sprintf( $message, 'WPML' ) . '

'; 20 | break; 21 | 22 | case 'no_st_notice' : 23 | $message = __( 'Multilingual Tools plugin is enabled but not effective. It requires WPML String Translation plugin in order to work.', 'wpml-compatibility-test-tools' ); 24 | echo '

' . sprintf( $message, 'WPML' ) . '

'; 25 | break; 26 | 27 | case 'no_selected_language_notice' : 28 | echo '

' . __( 'At least one language should be selected in order to translate strings.', 'wpml-compatibility-test-tools' ) . '

'; 29 | break; 30 | 31 | case 'no_selected_language_for_pages_notice' : 32 | echo '

' . __( 'At least one language should be selected in order to create pages with dummy content.', 'wpml-compatibility-test-tools' ) . '

'; 33 | break; 34 | 35 | case 'no_context_notice' : 36 | echo '

' . __( 'Please select the context.', 'wpml-compatibility-test-tools' ) . '

'; 37 | break; 38 | 39 | case 'no_template_notice' : 40 | echo '

' . __( 'Template is required.', 'wpml-compatibility-test-tools' ) . '

'; 41 | break; 42 | 43 | case 'settings_updated_notice' : 44 | echo '

' . __( 'Settings updated.', 'wpml-compatibility-test-tools' ) . '

'; 45 | break; 46 | 47 | case 'file_save_success' : 48 | echo '

' . __( 'File successfully saved in active theme folder.', 'wpml-compatibility-test-tools' ) . '

'; 49 | break; 50 | 51 | case 'duplicate_strings_available' : 52 | $message = sprintf( 53 | __( "Your settings have been updated.
Now, continue to the %s screen, select all the site's content, select Duplicate all and click on Send documents. %s.", 'wpml-compatibility-test-tools' ), 54 | "" . 55 | __( 'Translation Dashboard','wpml-compatibility-test-tools' ) . "", "Help" ); 56 | 57 | echo '

' . $message . '

'; 58 | break; 59 | 60 | case 'wctt_in_action_notice' : 61 | // Get current settings for string duplication 62 | $duplicate_strings = WPML_Compatibility_Test_Tools::get_option( 'duplicate_strings' ); 63 | // Prepare a message 64 | $message = 65 | __( "WPML Compatibility Tester plugin is running and will automatically add language information to all new duplicates for your site. Right now, it will add language information for the following post fields:", 'wpml-compatibility-test-tools' ) . "
" . 66 | ( isset( $duplicate_strings['post']['title'] ) ? '[✔] ' : '[ ] ' ) . __( 'Post title' , 'wpml-compatibility-test-tools' ) . "
" . 67 | ( isset( $duplicate_strings['post']['content'] ) ? '[✔] ' : '[ ] ' ) . __( 'Post content' , 'wpml-compatibility-test-tools' ) . "
" . 68 | ( isset( $duplicate_strings['post']['excerpt'] ) ? '[✔] ' : '[ ] ' ) . __( 'Post excerpt' , 'wpml-compatibility-test-tools' ) . "
" . 69 | ( isset( $duplicate_strings['custom_field']['value'] ) ? '[✔] ' : '[ ] ' ) . __( 'Custom fields', 'wpml-compatibility-test-tools' ) . "
" . 70 | ( isset( $duplicate_strings['taxonomy']['all'] ) ? '[✔] ' : '[ ] ' ) . __( 'Term name ' , 'wpml-compatibility-test-tools' ) . "
" . 71 | ( isset( $duplicate_strings['taxonomy_slug']['all'] ) ? '[✔] ' : '[ ] ' ) . __( 'Term slug' , 'wpml-compatibility-test-tools' ) . "
" . 72 | sprintf( "" . __( "Click here to change fields to duplicate", 'wpml-compatibility-test-tools') . "
", admin_url( 'admin.php?page=wctt' ) ) . "
" . 73 | __( "To proceed, select all the site's content, scroll down and select Duplicate content and then click on Duplicate.", 'wpml-compatibility-test-tools' ) . "
" . 74 | "
" . __( "Please note that any existing translations for selected posts will be overwritten!", 'wpml-compatibility-test-tools' ) . "
"; 75 | 76 | echo '

' . $message . '

'; 77 | break; 78 | 79 | case 'shortcode_debug_action_reset' : 80 | echo '

' . __( 'Cleared shortcode debug data.', 'wpml-compatibility-test-tools' ) . '

'; 81 | break; 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /inc/wpml-compatibility-test-tools.class.php: -------------------------------------------------------------------------------- 1 | messages, 'no_wpml_notice' ) ); 26 | } 27 | 28 | return false; 29 | } 30 | 31 | // Check for Translation Management 32 | if ( ! defined( 'WPML_TM_VERSION' ) ) { 33 | add_action( 'admin_notices', array( $this->messages, 'no_tm_notice' ) ); 34 | 35 | return false; 36 | } 37 | 38 | // Check for String Translation 39 | if ( ! defined( 'WPML_ST_VERSION' ) ) { 40 | add_action( 'admin_notices', array( $this->messages, 'no_st_notice' ) ); 41 | 42 | return false; 43 | } 44 | 45 | // WPML setup has to be finished 46 | global $sitepress; 47 | if ( ! isset( $sitepress ) ) { 48 | add_action( 'admin_notices', array( $this->messages, 'no_wpml_notice' ) ); 49 | 50 | return false; 51 | } 52 | 53 | if ( method_exists( $sitepress, 'get_setting' ) && ! $sitepress->get_setting( 'setup_complete' ) ) { 54 | add_action( 'admin_notices', array( $this->messages, 'not_finished_wpml_setup' ) ); 55 | 56 | return false; 57 | } 58 | 59 | self::install(); 60 | 61 | add_action( 'admin_menu', array( $this, 'register_administration_page' ) ); 62 | add_action( 'admin_enqueue_scripts', array( $this, 'add_scripts' ) ); 63 | add_action( 'admin_enqueue_scripts', array( $this, 'add_styles' ) ); 64 | add_action( 'wp_ajax_generate_strings_translations_action', array( $this, 'generate_strings_translations' ) ); 65 | 66 | // Handle admin settings page 67 | $this->process_request(); 68 | 69 | // Change WPML behaviour based on selected settings 70 | $this->modify_wpml_behaviour(); 71 | 72 | do_action( 'mltools_loaded' ); 73 | 74 | return true; 75 | } 76 | 77 | /** 78 | * Process admin settings page requests 79 | * 80 | * @return bool 81 | */ 82 | public function process_request() { 83 | $this->process_strings_auto_translate_action_translate(); 84 | $this->process_save_duplicate_strings_to_translate(); 85 | $this->process_save_shortcode_helper_settings(); 86 | 87 | return true; 88 | } 89 | 90 | /** 91 | * Process action strings_auto_translate_action_translate 92 | * 93 | * @return bool 94 | */ 95 | private function process_strings_auto_translate_action_translate() { 96 | if ( isset( $_POST['strings_auto_translate_action_save'] ) || isset( $_POST['strings_auto_translate_action_translate'] ) ) { 97 | 98 | $error = false; 99 | 100 | $contexts = ( isset( $_POST['strings_auto_translate_context'] ) ) ? $_POST['strings_auto_translate_context'] : ''; 101 | $languages = ( isset( $_POST['active_languages'] ) ) ? $_POST['active_languages'] : array(); 102 | $template = ( isset( $_POST['strings_auto_translate_template'] ) ) ? $_POST['strings_auto_translate_template'] : ''; 103 | 104 | if ( empty( $template ) ) { 105 | add_action( 'admin_notices', array( $this->messages, 'no_template_notice' ) ); 106 | $error = true; 107 | } 108 | 109 | if ( $error ) { 110 | return false; 111 | } 112 | 113 | self::update_option( 'string_auto_translate_context', $contexts ); 114 | self::update_option( 'string_auto_translate_languages', $languages ); 115 | self::update_option( 'string_auto_translate_template', $template ); 116 | 117 | add_action( 'admin_notices', array( $this->messages, 'settings_updated_notice' ) ); 118 | 119 | $contexts = self::get_option( 'string_auto_translate_context' ); 120 | $languages = self::get_option( 'string_auto_translate_languages' ); 121 | $template = self::get_option( 'string_auto_translate_template' ); 122 | 123 | if ( empty( $languages ) ) { 124 | add_action( 'admin_notices', array( $this->messages, 'no_selected_language_notice' ) ); 125 | $error = true; 126 | } 127 | 128 | if ( empty( $template ) ) { 129 | add_action( 'admin_notices', array( $this->messages, 'no_template_notice' ) ); 130 | $error = true; 131 | } 132 | 133 | if ( empty( $contexts ) ) { 134 | add_action( 'admin_notices', array( $this->messages, 'no_context_notice' ) ); 135 | $error = true; 136 | } 137 | 138 | if ( $error ) { 139 | return false; 140 | } 141 | } 142 | 143 | return true; 144 | } 145 | 146 | /** 147 | * Auto translate strings with given context 148 | * 149 | * @param $context 150 | * @param $languages 151 | * @param $template 152 | */ 153 | private function translate_strings( $strings, $languages, $template ) { 154 | // For each string add information 155 | foreach ( $strings as $v ) { 156 | foreach ( $languages as $lang ) { 157 | icl_add_string_translation( $v->id, $lang, wpml_ctt_prepare_string( $template, $v->value, $lang ), ICL_STRING_TRANSLATION_COMPLETE ); 158 | icl_update_string_status( $v->id ); 159 | } 160 | } 161 | } 162 | 163 | public function generate_strings_translations() { 164 | check_ajax_referer( 'mt_generate_strings_translations', '_mt_mighty_nonce' ); 165 | 166 | $contexts = isset( $_POST['contexts'] ) ? (array) $_POST['contexts'] : false; 167 | $languages = isset( $_POST['languages'] ) ? $_POST['languages'] : false; 168 | $template = isset( $_POST['template'] ) ? $_POST['template'] : false; 169 | $count = isset( $_POST['count'] ) ? $_POST['count'] : false; 170 | $offset = isset( $_POST['offset'] ) ? $_POST['offset'] : 0; 171 | 172 | // Check in case JS fail. 173 | if ( ! $contexts || ! $languages || ! $template ) { 174 | wp_send_json( 0 ); 175 | } 176 | 177 | // Strings batch threshold 178 | $limit = 100; 179 | 180 | global $wpdb; 181 | 182 | $esc_contexts = array_map( function ( $context ) { 183 | return "'" . esc_sql( $context ) . "'"; 184 | }, $contexts ); 185 | $esc_contexts = implode( ",", $esc_contexts ); 186 | 187 | // Skip count if process started. 188 | if ( $count === false ) { 189 | $count = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}icl_strings WHERE context IN ({$esc_contexts})" ); 190 | 191 | // Update settings only on first run. 192 | self::update_option( 'string_auto_translate_context', $contexts ); 193 | self::update_option( 'string_auto_translate_languages', $languages ); 194 | self::update_option( 'string_auto_translate_template', $template ); 195 | } 196 | 197 | if ( $offset <= $count ) { 198 | $strings = $wpdb->get_results( "SELECT id, language, context, value FROM {$wpdb->prefix}icl_strings WHERE context IN ({$esc_contexts}) LIMIT {$offset}, {$limit}" ); 199 | $this->translate_strings( $strings, $languages, $template ); 200 | 201 | // Update offset. 202 | $offset += $limit; 203 | 204 | // Calculate progress percentage. 205 | $strings_left_count = max( $count - $offset, 0 ); 206 | $progress = floor( 100 - $strings_left_count * 100 / $count ); 207 | 208 | wp_send_json( array( 209 | 'offset' => $offset, 210 | 'count' => $count, 211 | 'progress' => $progress 212 | ) ); 213 | } else { 214 | wp_send_json( 1 ); 215 | } 216 | } 217 | 218 | /** 219 | * Process action save_duplicate_strings_to_translate 220 | */ 221 | private function process_save_duplicate_strings_to_translate() { 222 | if ( isset( $_POST['save_duplicate_strings_to_translate'] ) ) { 223 | 224 | $error = false; 225 | 226 | $strings = ( isset( $_POST['duplicate_strings_to_translate'] ) ) ? $_POST['duplicate_strings_to_translate'] : array(); 227 | $template = ( isset( $_POST['duplicate_strings_template'] ) ) ? $_POST['duplicate_strings_template'] : ''; 228 | 229 | if ( empty( $template ) ) { 230 | add_action( 'admin_notices', array( $this->messages, 'no_template_notice' ) ); 231 | $error = true; 232 | } 233 | 234 | if ( $error ) { 235 | return false; 236 | } 237 | 238 | self::update_option( 'duplicate_strings', $strings ); 239 | self::update_option( 'duplicate_strings_template', $template ); 240 | 241 | if ( ! empty( $strings ) ) { 242 | add_action( 'admin_notices', array( $this->messages, 'duplicate_strings_available' ) ); 243 | } else { 244 | add_action( 'admin_notices', array( $this->messages, 'settings_updated_notice' ) ); 245 | } 246 | } 247 | 248 | return true; 249 | } 250 | 251 | private function process_save_shortcode_helper_settings() { 252 | 253 | if ( isset( $_POST['_mltools_shortcode_helper_nonce'] ) 254 | && wp_verify_nonce( $_POST['_mltools_shortcode_helper_nonce'], 'mltools_shortcode_helper_settings_save' ) ) { 255 | 256 | if ( isset( $_POST['shortcode_debug_action_save'] ) ) { 257 | 258 | $enable = isset( $_POST['shortcode_enable_debug'] ); 259 | self::update_option( 'shortcode_enable_debug', $enable ); 260 | 261 | $enable_debug_value = isset( $_POST['shortcode_enable_debug_value'] ); 262 | self::update_option( 'shortcode_enable_debug_value', $enable_debug_value ); 263 | 264 | add_action( 'admin_notices', array( $this->messages, 'settings_updated_notice' ) ); 265 | } 266 | if ( isset( $_POST['shortcode_debug_action_reset'] ) 267 | && class_exists( 'MLTools_Shortcode_Attribute_Filter' ) ) { 268 | 269 | delete_option( MLTools_Shortcode_Attribute_Filter::OPTION_NAME ); 270 | delete_option( MLTools_Shortcode_Attribute_Filter::OPTION_NAME_VALUES ); 271 | 272 | add_action( 'admin_notices', array( $this->messages, 'shortcode_debug_action_reset' ) ); 273 | } 274 | if ( isset( $_POST['shortcode_ignored_tags'] ) ) { 275 | self::update_option( 'shortcode_ignored_tags', sanitize_text_field( $_POST['shortcode_ignored_tags'] ) ); 276 | } 277 | 278 | if ( isset( $_POST['_wp_http_referer'] ) ) { 279 | wp_redirect( $_POST['_wp_http_referer'] ); 280 | die(); 281 | } 282 | } 283 | } 284 | 285 | /** 286 | * Modify WPML behaviour based on selected settings 287 | */ 288 | public function modify_wpml_behaviour() { 289 | 290 | // Enable adding language information for duplicated posts. 291 | $duplicate_strings = self::get_option( 'duplicate_strings' ); 292 | $duplicate_strings_template = self::get_option( 'duplicate_strings_template' ); 293 | 294 | if ( ! empty( $duplicate_strings ) && ! empty( $duplicate_strings_template ) ) { 295 | new Modify_Duplicate_Strings( $duplicate_strings, $duplicate_strings_template ); 296 | 297 | // Add information about the plugin settings to Translation Dashboard. 298 | if ( isset( $_GET['page'] ) && ( in_array( $_GET['page'], array( basename( WPML_TM_PATH ) . '/menu/main.php' ) ) ) ) { 299 | add_action( 'admin_notices', array( $this->messages, 'wctt_in_action_notice' ) ); 300 | } 301 | } 302 | } 303 | 304 | 305 | /** 306 | * Register settings page 307 | */ 308 | public function register_administration_page() { 309 | add_menu_page( __( 'Dashboard', 'wpml-compatibility-test-tools' ), __( 'Multilingual Tools', 'wpml-compatibility-test-tools' ), 'manage_options', 'mt', array( 310 | $this, 311 | 'load_template' 312 | ), WPML_CTT_PLUGIN_URL . '/res/img/wctt-icon.png' ); 313 | add_submenu_page( 'mt', __( 'Overview', 'wpml-compatibility-test-tools' ), __( 'Overview', 'wpml-compatibility-test-tools' ), 'manage_options', 'mt', array( 314 | $this, 315 | 'load_template' 316 | ) ); 317 | add_submenu_page( 'mt', __( 'Settings', 'wpml-compatibility-test-tools' ), __( 'Settings', 'wpml-compatibility-test-tools' ), 'manage_options', 'mt-settings', array( 318 | $this, 319 | 'load_template' 320 | ) ); 321 | add_submenu_page( 'mt', __( 'Configuration Generator', 'wpml-compatibility-test-tools' ), __( 'Configuration Generator', 'wpml-compatibility-test-tools' ), 'manage_options', 'mt-generator', array( 322 | $this, 323 | 'load_template' 324 | ) ); 325 | add_submenu_page( 'mt', __( 'Custom Field Settings Helper', 'wpml-compatibility-test-tools' ), __( 'Custom Field Settings Helper', 'wpml-compatibility-test-tools' ), 'manage_options', 'cf-translations', array( 326 | $this, 327 | 'load_template' 328 | ) ); 329 | } 330 | 331 | /** 332 | * Load page template 333 | */ 334 | public function load_template() { 335 | $screen = get_current_screen(); 336 | 337 | switch ( $screen->id ) { 338 | case 'toplevel_page_mt' : 339 | add_filter( 'wpml_config_array', array( $this, 'save_configuration_for_debug' ) ); 340 | add_filter( 'wpml_parse_config_file', array( $this, 'display_configuration_for_debug' ) ); 341 | 342 | require WPML_CTT_ABS_PATH . 'menus/settings/overview.php'; 343 | break; 344 | 345 | case 'multilingual-tools_page_mt-settings' : 346 | require WPML_CTT_ABS_PATH . 'menus/settings/settings.php'; 347 | break; 348 | 349 | case 'multilingual-tools_page_mt-generator' : 350 | require WPML_CTT_ABS_PATH . 'menus/settings/generator.php'; 351 | break; 352 | 353 | case 'multilingual-tools_page_cf-translations' : 354 | require WPML_CTT_ABS_PATH . 'menus/settings/custom-fields-translation.php'; 355 | break; 356 | } 357 | } 358 | 359 | public function js_labels() { 360 | return array( 361 | 'question' => __( "All existing strings translations will be replaced with new values.\n Are you sure you want to do this?", 'multilingual-tools' ), 362 | 'no_context_notice' => __( "* Please select the context.", 'multilingual-tools' ), 363 | 'no_selected_language_notice' => __( "* At least one language should be selected in order to translate strings.", 'multilingual-tools' ), 364 | 'no_template_notice' => __( "* Template is required.", 'multilingual-tools' ) 365 | ); 366 | } 367 | 368 | /** 369 | * Add scripts only for plugin pages 370 | */ 371 | public function add_scripts( $hook ) { 372 | if ( in_array( $hook, array( 373 | 'toplevel_page_mt', 374 | 'multilingual-tools_page_mt-settings', 375 | 'multilingual-tools_page_mt-generator' 376 | ) ) ) { 377 | wp_enqueue_script( 'mt-scripts', WPML_CTT_PLUGIN_URL . '/res/js/mt-script.js', array( 'jquery' ), WPML_CTT_VERSION ); 378 | wp_localize_script( 'mt-scripts', 'mt_data', array( 379 | 'ajax_url' => admin_url( 'admin-ajax.php' ), 380 | 'labels' => $this->js_labels() 381 | ) ); 382 | 383 | } 384 | 385 | elseif ($hook == 'multilingual-tools_page_cf-translations') { 386 | 387 | wp_enqueue_script( 388 | 'wpml_custom_fields_helper_script', 389 | WPML_CTT_PLUGIN_URL . '/res/js/mt-script.js', 390 | array( 'jquery' ), 391 | false, 392 | true 393 | ); 394 | 395 | wp_localize_script( 396 | 'wpml_custom_fields_helper_script', 397 | 'wpmlData', 398 | array( 399 | 'ajax_url' => admin_url( 'admin-ajax.php' ), 400 | ) 401 | ); 402 | 403 | } 404 | } 405 | 406 | /** 407 | * Add styles only for plugin pages 408 | */ 409 | public function add_styles( $hook ) { 410 | if ( in_array( $hook, array( 411 | 'toplevel_page_mt', 412 | 'multilingual-tools_page_mt-settings', 413 | 'multilingual-tools_page_mt-generator' 414 | ) ) ) { 415 | wp_register_style( 'wctt-generator-style', WPML_CTT_PLUGIN_URL . '/res/css/wctt-style.css', WPML_CTT_VERSION ); 416 | wp_enqueue_style( 'wctt-generator-style' ); 417 | } 418 | } 419 | 420 | /** 421 | * Generate XML file 422 | * 423 | * Generation wpml-config.xml file. 424 | * Used as configuration file for WPML plugin. 425 | * 426 | * @url https://wpml.org/documentation/support/language-configuration-files/ 427 | */ 428 | public function generate_xml() { 429 | $dom = new DOMDocument(); 430 | $dom->preserveWhiteSpace = false; 431 | $dom->formatOutput = true; 432 | 433 | $root = $dom->createElement( 'wpml-config' ); 434 | $root = $dom->appendChild( $root ); 435 | 436 | $args = array( 437 | '_builtin' => false 438 | ); 439 | 440 | $post_types = get_post_types( $args, 'names' ); 441 | $checkbox_cpt = isset( $_POST['_cpt'] ) && is_array( $_POST['_cpt'] ) ? $_POST['_cpt'] : null; 442 | $radio_cpt = isset( $_POST['cpt'] ) && is_array( $_POST['cpt'] ) ? $_POST['cpt'] : null; 443 | 444 | $taxonomies = get_taxonomies( $args ); 445 | $checkbox_tax = isset( $_POST['_tax'] ) && is_array( $_POST['_tax'] ) ? $_POST['_tax'] : null; 446 | $radio_tax = isset( $_POST['tax'] ) && is_array( $_POST['tax'] ) ? $_POST['tax'] : null; 447 | 448 | $custom_fields = wpml_get_custom_fields(); 449 | $checkbox_cf = isset( $_POST['_cf'] ) && is_array( $_POST['_cf'] ) ? $_POST['_cf'] : null; 450 | $radio_cf = isset( $_POST['cf'] ) && is_array( $_POST['cf'] ) ? $_POST['cf'] : null; 451 | 452 | if ( $checkbox_cpt ) { 453 | $this->generate_basic_content_types( 454 | $dom, 455 | $root, 456 | $post_types, 457 | $checkbox_cpt, 458 | $radio_cpt, 459 | 'custom-types', 460 | 'custom-type', 461 | 'translate' 462 | ); 463 | } 464 | 465 | if ( $checkbox_tax ) { 466 | $this->generate_basic_content_types( 467 | $dom, 468 | $root, 469 | $taxonomies, 470 | $checkbox_tax, 471 | $radio_tax, 472 | 'taxonomies', 473 | 'taxonomy', 474 | 'translate' 475 | ); 476 | } 477 | 478 | if ( $checkbox_cf ) { 479 | $this->generate_basic_content_types( 480 | $dom, 481 | $root, 482 | $custom_fields, 483 | $checkbox_cf, 484 | $radio_cf, 485 | 'custom-fields', 486 | 'custom-field', 487 | 'action' 488 | ); 489 | } 490 | 491 | if ( isset( $_POST['at'] ) ) { 492 | $this->generate_admin_texts( $dom, $root ); 493 | } 494 | 495 | if ( isset( $_POST['shc'] ) && is_array( $_POST['shc'] ) ) { 496 | $this->generate_shortcodes( $dom, $root ); 497 | } 498 | 499 | $xml = $dom->saveXML( $root ); 500 | 501 | // Save options 502 | switch ( wpml_ctt_validate_radio( $_POST['save'] ) ) { 503 | case 'file' : 504 | header( "Content-Description: File Transfer" ); 505 | header( 'Content-Disposition: attachment; filename="wpml-config.xml"' ); 506 | header( "Content-Type: application/xml" ); 507 | echo $xml; 508 | die(); 509 | break; 510 | 511 | case 'dir' : 512 | if ( file_put_contents( get_template_directory() . '/wpml-config.xml', $xml ) ) { 513 | add_action( 'admin_notices', array( $this->messages, 'file_save_success' ) ); 514 | } 515 | break; 516 | } 517 | } 518 | 519 | /** 520 | * Generate XML from option array 521 | * 522 | * @param $options 523 | * @param $node 524 | * @param $dom 525 | * 526 | * @since 1.3.0 527 | * 528 | */ 529 | public function option2xml( $options, $node, $dom ) { 530 | if ( is_array( $options ) ) { 531 | 532 | foreach ( $options as $option => $value ) { 533 | 534 | // Only if parent option is selected, both parent and child will be generated 535 | if ( isset( $_POST['at'][ $option ] ) ) { 536 | $at = $node->appendChild( $dom->createElement( 'key' ) ); 537 | $atatr = $dom->createAttribute( 'name' ); 538 | $atatr->value = $option; 539 | $at->appendChild( $atatr ); 540 | 541 | if ( is_array( $value ) ) { 542 | $this->option2xml( $value, $at, $dom ); 543 | } 544 | } 545 | } 546 | } 547 | } 548 | 549 | /** 550 | * Generate DOM nodes for basic content types. 551 | * 552 | * Basic content types in this case are: custom post types, taxonomies, custom fields. 553 | * 554 | * @param $dom 555 | * @param $root 556 | * @param $content 557 | * @param $checkbox 558 | * @param $radio 559 | * @param $parent 560 | * @param $child 561 | * @param $attribute 562 | * 563 | * @since 1.3.0 564 | * 565 | */ 566 | public function generate_basic_content_types( $dom, $root, $content, $checkbox, $radio, $parent, $child, $attribute ) { 567 | $parent_node = $dom->createElement( $parent ); 568 | $parent_node = $root->appendChild( $parent_node ); 569 | 570 | foreach ( $content as $c ) { 571 | 572 | if ( $parent === 'custom-fields' ) { 573 | $c = $c->meta_key; 574 | } 575 | 576 | if ( isset( $checkbox[ $c ] ) ) { 577 | $child_node = $dom->createElement( $child, sanitize_key( $c ) ); 578 | $child_node = $parent_node->appendChild( $child_node ); 579 | $child_node_attr = $dom->createAttribute( $attribute ); 580 | $child_node_attr->value = wpml_ctt_validate_radio( $radio[ $c ] ); 581 | $child_node->appendChild( $child_node_attr ); 582 | 583 | // When set to display as translated. 584 | if ( $radio[ $c ] === '2' ) { 585 | $child_node_attr = $dom->createAttribute( 'display-as-translated' ); 586 | $child_node_attr->value = '1'; 587 | $child_node->appendChild( $child_node_attr ); 588 | } 589 | } 590 | } 591 | } 592 | 593 | /** 594 | * Generate DOM nodes for admin texts 595 | * 596 | * @param $dom 597 | * @param $root 598 | * 599 | * @since 1.3.0 600 | * 601 | */ 602 | public function generate_admin_texts( $dom, $root ) { 603 | $ats = $dom->createElement( 'admin-texts' ); 604 | $ats = $root->appendChild( $ats ); 605 | 606 | $options = wpml_ctt_options_list(); 607 | 608 | foreach ( $options as $name => $value ) { 609 | $options[ $name ] = maybe_unserialize( maybe_unserialize( $value ) ); 610 | } 611 | 612 | $this->option2xml( $options, $ats, $dom ); 613 | } 614 | 615 | /** 616 | * Generate DOM nodes for shortcodes 617 | * 618 | * @param $dom 619 | * @param $root 620 | * 621 | * @since 1.3.0 622 | * 623 | */ 624 | public function generate_shortcodes( $dom, $root ) { 625 | $shortcodes = array_unique( $_POST['shc'] ); 626 | $shortcode_attr = isset( $_POST['shc-attr'] ) && is_array( $_POST['shc-attr'] ) ? (array) $_POST['shc-attr'] : null; 627 | 628 | // Create xml node 629 | $shortcodes_node = $dom->createElement( 'shortcodes' ); 630 | $shortcodes_node = $root->appendChild( $shortcodes_node ); 631 | 632 | foreach ( $shortcodes as $shortcode ) { 633 | 634 | $shortcode_index = array_search( $shortcode, $shortcodes, true ); 635 | $shortcode = str_replace( ' ', '', sanitize_html_class( $shortcode, "Invalid_shortcode" ) ); 636 | 637 | $shortcode_node = $dom->createElement( 'shortcode' ); 638 | $shortcode_node = $shortcodes_node->appendChild( $shortcode_node ); 639 | 640 | $tag_node = $dom->createElement( 'tag', $shortcode ); 641 | $shortcode_node->appendChild( $tag_node ); 642 | 643 | if ( ! is_null( $shortcode_attr ) && $shortcode_attr[ $shortcode_index ] !== "" ) { 644 | 645 | $attribute = str_replace( ' ', '', $shortcode_attr[ $shortcode_index ] ); 646 | $attributes_array = explode( ",", $attribute ); 647 | 648 | // Dealing with shortcode attribute if available. 649 | $attributes_node = $dom->createElement( 'attributes' ); 650 | $attributes_node = $shortcode_node->appendChild( $attributes_node ); 651 | 652 | if ( ! empty( $attributes_array ) ) { 653 | 654 | foreach ( $attributes_array as $a ) { 655 | $attribute_node = $dom->createElement( 'attribute', sanitize_html_class( $a, "Invalid_attribute" ) ); 656 | $attributes_node->appendChild( $attribute_node ); 657 | } 658 | 659 | } else { 660 | $attribute_node = $dom->createElement( 'attribute', sanitize_html_class( $attribute, "Invalid_attribute" ) ); 661 | $attributes_node->appendChild( $attribute_node ); 662 | } 663 | } 664 | } 665 | } 666 | 667 | /** 668 | * Save current configuration in a global variable to display later. 669 | * 670 | * @param array $config 671 | * 672 | * @return array 673 | * @global array $wpml_config_debug 674 | */ 675 | function save_configuration_for_debug( $config ) { 676 | global $wpml_config_debug; 677 | 678 | // Check which sections have content and assign a title for each section. 679 | $wpml_config_debug = array(); 680 | if ( ! empty( $config['wpml-config']['custom-types']['custom-type'] ) ) { 681 | $wpml_config_debug['Custom posts'] = $config['wpml-config']['custom-types']['custom-type']; 682 | } 683 | if ( ! empty( $config['wpml-config']['taxonomies']['taxonomy'] ) ) { 684 | $wpml_config_debug['Custom taxonomies'] = $config['wpml-config']['taxonomies']['taxonomy']; 685 | } 686 | if ( ! empty( $config['wpml-config']['custom-fields']['custom-field'] ) ) { 687 | $wpml_config_debug['Custom fields translation'] = $config['wpml-config']['custom-fields']['custom-field']; 688 | } 689 | if ( ! empty( $config['wpml-config']['custom-term-fields']['custom-term-field'] ) ) { 690 | $wpml_config_debug['Custom Term Meta Translation'] = $config['wpml-config']['custom-term-fields']['custom-term-field']; 691 | } 692 | if ( ! empty( $config['wpml-config']['shortcodes']['shortcode'] ) ) { 693 | $wpml_config_debug['Shortcodes'] = $config['wpml-config']['shortcodes']['shortcode']; 694 | } 695 | if ( ! empty( $config['wpml-config']['admin-texts']['key'] ) ) { 696 | $wpml_config_debug['Admin Strings to Translate'] = $config['wpml-config']['admin-texts']['key']; 697 | } 698 | if ( ! empty( $config['wpml-config']['language-switcher-settings']['key'] ) ) { 699 | $wpml_config_debug['Language Switcher Settings'] = $config['wpml-config']['language-switcher-settings']['key']; 700 | } 701 | 702 | return $config; 703 | } 704 | 705 | /** 706 | * Intercept wpml-config.xml parsing to display loaded configuration files 707 | * for debugging purposes. 708 | * 709 | * @param string $file 710 | * 711 | * @return string 712 | * @global object $sitepress 713 | */ 714 | function display_configuration_for_debug( $file ) { 715 | // Get url and name. 716 | if ( is_object( $file ) ) { 717 | $url = ICL_REMOTE_WPML_CONFIG_FILES_INDEX . 'wpml-config/' . $file->admin_text_context . '/wpml-config.xml'; 718 | $name = $file->admin_text_context; 719 | $class = 'dashicons-admin-site'; 720 | } else { 721 | $url = str_replace( WP_CONTENT_DIR, WP_CONTENT_URL, $file ); 722 | $name = basename( dirname( $url ) ); 723 | $class = ''; 724 | } 725 | 726 | // Display link to file. 727 | echo '' . $name . ''; 728 | if ( ! empty( $class ) ) { 729 | echo ' '; 730 | } 731 | echo '
'; 732 | 733 | // Display validation errors if any found. 734 | if ( is_string( $file ) && file_exists( $file ) ) { 735 | $validate = new WPML_XML_Config_Validate( WPML_PLUGIN_PATH . '/res/xsd/wpml-config.xsd' ); 736 | $validate->from_file( $file ); 737 | $errors = wp_list_pluck( $validate->get_errors(), 'message' ); 738 | if ( ! empty( $errors ) ) { 739 | $errors = array_unique( $errors ); 740 | // TODO: add some style. 741 | echo '

' . implode( '
', $errors ) . '

'; 742 | } 743 | } 744 | 745 | return $file; 746 | } 747 | 748 | } 749 | -------------------------------------------------------------------------------- /inc/wpml-compatibility-test-tools.functions.php: -------------------------------------------------------------------------------- 1 | get_language_details( $lang ); 18 | 19 | if ( isset( $language_details['english_name'] ) ) { 20 | $template = str_replace( '%language_name%', $language_details['english_name'], $template ); 21 | } 22 | 23 | if ( isset( $language_details['code'] ) ) { 24 | $template = str_replace( '%language_code%', $language_details['code'], $template ); 25 | } 26 | 27 | if ( isset( $language_details['display_name'] ) ) { 28 | $template = str_replace( '%language_native_name%', $language_details['display_name'], $template ); 29 | } 30 | 31 | return $template; 32 | } 33 | 34 | /** 35 | * 36 | * Return list of contexts for string translation 37 | * 38 | * @return mixed 39 | */ 40 | function wpml_ctt_st_contexts() { 41 | return icl_st_get_contexts( false ); 42 | } 43 | 44 | /** 45 | * 46 | * Generate language checkboxes 47 | * 48 | * @param array $selected_languages - arrach of languages (code) that should be checked 49 | * 50 | * @return string 51 | */ 52 | function wpml_ctt_active_languages_output( $selected_languages = array() ) { 53 | $active_langs = apply_filters( 'wpml_active_languages', NULL, 'orderby=id&order=asc' ); 54 | $default_lang = apply_filters( 'wpml_default_language', NULL ); 55 | 56 | // Remove default language from list. 57 | unset( $active_langs[$default_lang] ); 58 | 59 | if ( empty( $active_langs ) ) { 60 | return sprintf( __( 'No active languages set. You can enable languages here.', 'wpml-compatibility-test-tools' ), admin_url( 'admin.php?page=sitepress-multilingual-cms/menu/languages.php' ) ); 61 | } 62 | 63 | $theme_lang_inputs = ''; 76 | 77 | return $theme_lang_inputs; 78 | } 79 | 80 | /** 81 | * 82 | * Return names of all custom fields 83 | * 84 | * @return mixed 85 | */ 86 | function wpml_get_custom_fields() { 87 | global $wpdb; 88 | 89 | return $wpdb->get_results( "SELECT DISTINCT(meta_key) FROM $wpdb->postmeta" ); 90 | } 91 | 92 | /** 93 | * 94 | * Returning through AJAX selected option array as JSON. 95 | * 96 | */ 97 | add_action( 'wp_ajax_wpml_ctt_action', 'wpml_ctt_options_list_ajax' ); 98 | function wpml_ctt_options_list_ajax() { 99 | check_ajax_referer( 'wctt-generate', '_wctt_mighty_nonce' ); 100 | 101 | $data = array(); 102 | $options = isset( $_POST['options'] ) ? (array) $_POST['options'] : array(); 103 | 104 | $safe_options = wpml_ctt_options_list(); 105 | 106 | foreach ( $options as $option ) { 107 | // Dealing with unwanted. 108 | if ( ! array_key_exists( $option, $safe_options ) ) { 109 | $data = ["{$option}" => 'No way Jose!']; 110 | break; 111 | } 112 | 113 | // Dealing with bad nested serialization. 114 | if ( ! is_serialized( get_option( $option ) ) ) { 115 | $data[ $option ] = get_option( $option ); 116 | } else { 117 | $data[ $option ] = '*** WARNING: NESTED SERIALIZATION DETECTED, WILL NOT WORK WITH WPML! ***'; 118 | } 119 | } 120 | 121 | echo json_encode( $data ); 122 | wp_die(); 123 | } 124 | 125 | /** 126 | * 127 | * Creating options list by filtering results from wp_options table. 128 | * 129 | * @return array 130 | * 131 | */ 132 | function wpml_ctt_options_list() { 133 | $exclude_list = array( 134 | 135 | /* WP default ones */ 136 | 137 | 'siteurl', 138 | 'home', 139 | 'blogname', 140 | 'blogdescription', 141 | 'users_can_register', 142 | 'admin_email', 143 | 'start_of_week', 144 | 'use_balanceTags', 145 | 'use_smilies', 146 | 'require_name_email', 147 | 'comments_notify', 148 | 'posts_per_rss', 149 | 'rss_use_excerpt', 150 | 'mailserver_url', 151 | 'mailserver_login', 152 | 'mailserver_pass', 153 | 'mailserver_port', 154 | 'default_category', 155 | 'default_comment_status', 156 | 'default_ping_status', 157 | 'default_pingback_flag', 158 | 'posts_per_page', 159 | 'date_format', 160 | 'time_format', 161 | 'links_updated_date_format', 162 | 'comment_moderation', 163 | 'moderation_notify', 164 | 'permalink_structure', 165 | 'gzipcompression', 166 | 'hack_file', 167 | 'blog_charset', 168 | 'active_plugins', 169 | 'category_base', 170 | 'ping_sites', 171 | 'advanced_edit', 172 | 'comment_max_links', 173 | 'gmt_offset', 174 | 'default_email_category', 175 | 'template', 176 | 'stylesheet', 177 | 'comment_whitelist', 178 | 'comment_registration', 179 | 'html_type', 180 | 'use_trackback', 181 | 'default_role', 182 | 'db_version', 183 | 'uploads_use_yearmonth_folders', 184 | 'upload_path', 185 | 'blog_public', 186 | 'default_link_category', 187 | 'show_on_front', 188 | 'tag_base', 189 | 'show_avatars', 190 | 'avatar_rating', 191 | 'upload_url_path', 192 | 'thumbnail_size_w', 193 | 'thumbnail_size_h', 194 | 'thumbnail_crop', 195 | 'medium_size_w', 196 | 'medium_size_h', 197 | 'avatar_default', 198 | 'large_size_w', 199 | 'large_size_h', 200 | 'image_default_link_type', 201 | 'image_default_size', 202 | 'image_default_align', 203 | 'close_comments_for_old_posts', 204 | 'close_comments_days_old', 205 | 'thread_comments', 206 | 'thread_comments_depth', 207 | 'page_comments', 208 | 'comments_per_page', 209 | 'default_comments_page', 210 | 'comment_order', 211 | 'sticky_posts', 212 | 'widget_categories', 213 | 'widget_text', 214 | 'widget_rss', 215 | 'timezone_string', 216 | 'page_for_posts', 217 | 'page_on_front', 218 | 'default_post_format', 219 | 'link_manager_enabled', 220 | 'initial_db_version', 221 | 'wp_user_roles', 222 | 'widget_search', 223 | 'widget_recent-posts', 224 | 'widget_recent-comments', 225 | 'widget_archives', 226 | 'widget_meta', 227 | 'sidebars_widgets', 228 | 'cron', 229 | 'rewrite_rules', 230 | 'can_compress_scripts', 231 | 'recently_activated', 232 | 'blacklist_keys', 233 | 'moderation_keys', 234 | 'links_recently_updated_prepend', 235 | 'links_recently_updated_append', 236 | 'links_recently_updated_time', 237 | 'embed_autourls', 238 | 'embed_size_w', 239 | 'embed_size_h', 240 | 'secret', 241 | 'use_linksupdate', 242 | 'rss_language', 243 | 'default_post_edit_rows', 244 | 'enable_app', 245 | 'enable_xmlrpc', 246 | 'recently_edited', 247 | 'auto_core_update_notified', 248 | 'db_upgraded', 249 | 250 | /* WPML added ones */ 251 | 252 | '_icl_cache', 253 | '_wpml_media', 254 | 'icl_adl_settings', 255 | 'icl_admin_messages', 256 | '_icl_admin_option_names', 257 | 'icl_sitepress_settings', 258 | 'icl_sitepress_version', 259 | 'icl_translation_jobs_basket', 260 | 'widget_icl_lang_sel_widget', 261 | 'wp_icl_non_translators_cached', 262 | 'wp_icl_translators_cached', 263 | 'wpml_config_files_arr', 264 | 'wpml_config_index', 265 | 'wpml_config_index_updated', 266 | 'wpml-package-translation-db-updates-run', 267 | 'wpml-package-translation-refresh-required', 268 | 'wpml-package-translation-string-packages-table-updated', 269 | 'wpml-package-translation-string-table-updated', 270 | 'WPML_CMS_NAV_VERSION', 271 | 'wpml_tm_version', 272 | 'wp_installer_settings', 273 | 'wpml_cms_nav_settings', 274 | 'wpml_ctt_settings' ); 275 | 276 | $options = wpml_ctt_load_alloptions(); 277 | 278 | foreach ( $options as $name => $value ) { 279 | if ( in_array( $name, $exclude_list ) || ( ! stristr( $name, '_transient' ) === false ) ) { 280 | unset( $options[$name] ); 281 | } 282 | } 283 | 284 | return $options; 285 | } 286 | 287 | /** 288 | * 289 | * Validate radio values. 290 | * 291 | * @param $value 292 | * 293 | * @return mixed 294 | */ 295 | function wpml_ctt_validate_radio( $value ) { 296 | $allowed = array( 297 | 'translate', 298 | 'copy-once', 299 | 'ignore', 300 | 'copy', 301 | 'file', 302 | 'dir', 303 | '2', 304 | '1', 305 | '0' 306 | ); 307 | 308 | if ( in_array( $value, $allowed, true ) ) { 309 | // When set to display as translated. 310 | if ( $value === '2' ) { 311 | return '1'; 312 | } 313 | 314 | return $value; 315 | } 316 | 317 | return ''; 318 | } 319 | 320 | /** 321 | * Loads and caches all options. 322 | * 323 | * @global wpdb $wpdb WordPress database abstraction object. 324 | * 325 | * @return array List of all options. 326 | */ 327 | function wpml_ctt_load_alloptions() { 328 | global $wpdb; 329 | 330 | if ( ! wp_installing() || ! is_multisite() ) { 331 | $alloptions = wp_cache_get( 'wpml_ctt_all_options', 'options' ); 332 | } else { 333 | $alloptions = false; 334 | } 335 | 336 | if ( ! $alloptions ) { 337 | $suppress = $wpdb->suppress_errors(); 338 | $alloptions_db = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options ORDER BY option_name" ); 339 | 340 | $wpdb->suppress_errors( $suppress ); 341 | 342 | $alloptions = array(); 343 | 344 | foreach ( (array) $alloptions_db as $o ) { 345 | $alloptions[ $o->option_name ] = $o->option_value; 346 | } 347 | if ( ! wp_installing() || ! is_multisite() ) { 348 | wp_cache_add( 'wpml_ctt_all_options', $alloptions, 'options' ); 349 | } 350 | } 351 | 352 | return $alloptions; 353 | } 354 | 355 | /** 356 | * Display an entry from a wpml-config.xml file. 357 | * 358 | * @param array $entry 359 | */ 360 | function wpml_ctt_parse_entry( $entry ) { 361 | if ( isset( $entry['tag']['value'] ) ) { 362 | // This is for items from the shortcodes section. 363 | echo '' . $entry['tag']['value'] . ''; 364 | if ( ! empty( $entry['attributes']['attribute'] ) ) { 365 | if ( isset( $entry['attributes']['attribute']['value'] ) ) { 366 | $entry['attributes']['attribute'] = array( $entry['attributes']['attribute'] ); 367 | } 368 | $attributes = wp_list_pluck($entry['attributes']['attribute'], 'value' ); 369 | echo ': ' . implode( ', ', $attributes ); 370 | } 371 | echo '
'; 372 | } else if ( isset( $entry['attr']['name'] ) ) { 373 | // This part if for admin-texts and language-switcher-settings. 374 | echo '' . $entry['attr']['name'] . ': '; 375 | echo $entry['value'] . '
'; 376 | if ( ! empty( $entry['key'] ) ) { 377 | echo '
'; 378 | foreach ( $entry['key'] as $key ) { 379 | wpml_ctt_parse_entry( $key ); 380 | } 381 | echo '
'; 382 | } 383 | } else { 384 | // This is for any other type of entry. 385 | echo '' . $entry['value'] . ': '; 386 | foreach ( $entry['attr'] as $key => $value ) { 387 | echo $key . ' => ' . $value . '
'; 388 | } 389 | } 390 | } 391 | 392 | function mltools_shortcode_helper_add_hooks() { 393 | 394 | $debug_enabled = WPML_Compatibility_Test_Tools::get_option( 'shortcode_enable_debug', false ); 395 | 396 | if ( $debug_enabled && is_user_logged_in() ) { 397 | 398 | $debug_values_enabled = WPML_Compatibility_Test_Tools::get_option( 'shortcode_enable_debug_value', false ); 399 | $default_ignored_tags = mltools_shortcode_helper_get_default_ignored_tags(); 400 | $ignored_tags = array_merge( $default_ignored_tags, array_map( 'trim', 401 | explode( ',', WPML_Compatibility_Test_Tools::get_option( 'shortcode_ignored_tags', '' ) ) 402 | ) ); 403 | 404 | $shortcode_attribute_filter = new MLTools_Shortcode_Attribute_Filter( $ignored_tags ); 405 | $shortcode_attribute_filter->add_hooks(); 406 | 407 | MLTools_Shortcode_WPML_Config_Parser::add_hooks(); 408 | 409 | if ( ! is_admin() ) { 410 | add_action( 'shutdown', 'mltools_shortcode_helper_unregistered_print_xml', 20 ); 411 | if ( $debug_values_enabled ) { 412 | add_action( 'shutdown', 'mltools_shortcode_helper_unregistered_print_captured_values', 30 ); 413 | } 414 | } 415 | 416 | } 417 | } 418 | 419 | function mltools_shortcode_helper_get_default_ignored_tags() { 420 | $default = array( 421 | 'vc_row', 422 | 'vc_column', 423 | 'vc_row_inner', 424 | 'vc_column_inner', 425 | 'vc_basic_grid', 426 | 'vc_empty_space', 427 | 'vc_icon', 428 | 'vc_separator', 429 | 'audio', 430 | 'caption', 431 | 'embed', 432 | 'gallery', 433 | 'playlist', 434 | 'video', 435 | 'wp_caption', 436 | 'wpml-string', 437 | 'wpml_language_form_field', 438 | 'wpml_language_selector_footer', 439 | 'wpml_language_selector_widget', 440 | 'wpml_language_switcher', 441 | ); 442 | sort( $default ); 443 | 444 | return $default; 445 | } 446 | 447 | function mltools_shortcode_helper_unregistered_print_xml() { 448 | 449 | $output = mltools_shortcode_helper_unregistered_get_xml_output(); 450 | 451 | if ( $output === false ) { 452 | 453 | user_error( 'MLTools shortcode helper: WPML_Config not loaded' ); 454 | 455 | } elseif ( is_string( $output ) && ! empty( $output ) ) { 456 | 457 | echo '
'
458 | 		     . htmlentities( $output ) . '
'; 459 | } 460 | } 461 | 462 | function mltools_shortcode_helper_unregistered_print_captured_values(){ 463 | 464 | $unregistered_tags = mltools_shortcode_helper_get_unregistered_tags(); 465 | $captured_values = get_option( MLTools_Shortcode_Attribute_Filter::OPTION_NAME_VALUES, array() ); 466 | 467 | foreach ( $captured_values as $tag => $values ) { 468 | 469 | if ( array_key_exists( $tag, $unregistered_tags )) { 470 | 471 | echo '
' . $tag . '
'; 478 | } 479 | } 480 | } 481 | 482 | /** 483 | * @return bool|string 484 | */ 485 | function mltools_shortcode_helper_unregistered_get_xml_output() { 486 | 487 | $xml_helper = new MLTools_XML_Helper(); 488 | $captured_tags = mltools_shortcode_helper_get_unregistered_tags(); 489 | 490 | if ( $captured_tags === false ) { 491 | return false; 492 | } 493 | 494 | if ( is_array( $captured_tags ) && ! empty( $captured_tags ) ) { 495 | return $xml_helper->get_dom_shortcodes( $captured_tags ); 496 | } 497 | 498 | return ''; 499 | } 500 | 501 | /** 502 | * @return bool|array 503 | */ 504 | function mltools_shortcode_helper_get_unregistered_tags() { 505 | 506 | $wpml_config = MLTools_Shortcode_WPML_Config_Parser::get_config(); 507 | 508 | if ( $wpml_config === false ) { 509 | return false; 510 | } 511 | 512 | $captured_tags = get_option( MLTools_Shortcode_Attribute_Filter::OPTION_NAME, array() ); 513 | $default_ignored_tags = mltools_shortcode_helper_get_default_ignored_tags(); 514 | $ignored_tags = array_merge( $default_ignored_tags, array_map( 'trim', 515 | explode( ',', WPML_Compatibility_Test_Tools::get_option( 'shortcode_ignored_tags', '' ) ) 516 | ) ); 517 | 518 | foreach ( $captured_tags as $tag => $config ) { 519 | if ( array_key_exists( $tag, $wpml_config ) || in_array( $tag, $ignored_tags ) ) { 520 | unset( $captured_tags[ $tag ] ); 521 | } 522 | } 523 | 524 | return $captured_tags; 525 | } -------------------------------------------------------------------------------- /inc/wpml-modify-duplicate-strings.class.php: -------------------------------------------------------------------------------- 1 | filter = $filter; 17 | $this->template = $template; 18 | add_filter( 'wpml_duplicate_generic_string', array( $this, 'duplicate_generic_string' ), 10, 3 ); 19 | } 20 | 21 | /** 22 | * 23 | * Add information about language to string based on context 24 | * 25 | * @param $string - string to modify 26 | * @param $lang - language code 27 | * @param $context - array( 28 | * 'context' => 'post' or 'custom_field' or 'taxonomy', 29 | * 'attribute' => 'title' or 'content' or 'excerpt' (for a post), 'value' (for a custom field), '{taxonomy_name}' (for a taxonomy), 30 | * 'key' => '{post_id}' | '{meta_key}' | '{term_id}', 31 | * ); 32 | * 33 | * @return string 34 | */ 35 | public function duplicate_generic_string( $string, $lang, $context ) { 36 | 37 | // Check context 38 | $filter_context = isset( $context['context'] ) ? $context['context'] : ''; 39 | $attribute = isset( $context['attribute'] ) ? $context['attribute'] : ''; 40 | 41 | // Check if user required to filter given string type (based on selected settings in admin panel) 42 | if ( isset( $this->filter[ $filter_context ] ) ) { 43 | // Special case for taxonomy 44 | if ( in_array( $filter_context, array( 'taxonomy', 'taxonomy_slug' ) ) ) { 45 | if ( ! isset( $this->filter[ $filter_context ]['all'] ) ) { 46 | return $string; 47 | } 48 | } elseif ( ! isset( $this->filter[ $filter_context ][ $attribute ] ) ) { 49 | return $string; 50 | } 51 | 52 | } else { 53 | return $string; 54 | } 55 | 56 | // Based on context 57 | switch ( $filter_context ) { 58 | case 'post': 59 | 60 | // Exception for empty excerpt field 61 | if ( ( 0 === strcmp( $attribute, 'excerpt' ) ) && ( empty( $string ) ) ) { 62 | break; 63 | } 64 | 65 | $string = wpml_ctt_prepare_string( $this->template, $string, $lang ); 66 | break; 67 | case 'taxonomy': 68 | $string = wpml_ctt_prepare_string( $this->template, $string, $lang ); 69 | break; 70 | case 'taxonomy_slug' : 71 | $string = $this->add_language_name_to_slug( $string, $lang ); 72 | break; 73 | case 'custom_field' : 74 | $string = $this->add_language_name_to_custom_field( $string, $lang, $context ); 75 | break; 76 | } 77 | 78 | // By default return the same value 79 | return $string; 80 | } 81 | 82 | /** 83 | * Add language name to slug 84 | * 85 | * @param $string 86 | * @param $lang 87 | * 88 | * @return string 89 | */ 90 | private function add_language_name_to_slug( $string, $lang ) { 91 | global $sitepress; 92 | 93 | $language_details = $sitepress->get_language_details( $lang ); 94 | 95 | if ( isset( $language_details['english_name'] ) ) { 96 | return sanitize_title_with_dashes( $language_details['english_name'] . '-' . $string, 'save' ); 97 | } 98 | 99 | return $string; 100 | } 101 | 102 | /** 103 | * 104 | * Add language name to custom field (only if is set to translate) 105 | * 106 | * @param $string 107 | * @param $lang 108 | * @param $context 109 | * 110 | * @return string 111 | */ 112 | private function add_language_name_to_custom_field( $string, $lang, $context ) { 113 | 114 | // Get settings - $this->settings is not set when creating duplicate (not updating) 115 | global $sitepress_settings; 116 | $settings =& $sitepress_settings['translation-management']; 117 | 118 | // Check for custom fields to translate 119 | if ( isset( $settings['custom_fields_translation'] ) ) { 120 | // Get information about custom fields to translate 121 | $custom_fields_translation = $settings['custom_fields_translation']; 122 | 123 | if ( isset( $custom_fields_translation[$context['key']] ) ) { 124 | 125 | // If custom field is set to translate (id = 2) 126 | if ( $custom_fields_translation[$context['key']] == 2 ) { 127 | // Add language information 128 | return wpml_ctt_prepare_string( $this->template, $string, $lang ); 129 | } 130 | } 131 | } 132 | 133 | return $string; 134 | } 135 | } -------------------------------------------------------------------------------- /menus/settings/auto-translate-duplicate.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 51 | 52 | 53 |

10 |

" . __( 'Translation Dashboard', 'wpml-compatibility-test-tools' ) . "" ); ?>

11 |
15 |
16 | 17 | 18 | 19 |
    20 |
  • />
  • 21 |
  • />
  • 22 |
  • />
  • 23 |
  • />
  • 24 |
  • />
  • 25 |
  • />
  • 26 | Toggle all 27 |
28 | 29 | 30 |
31 |
32 | 33 |
34 |
35 | 44 |
45 | You can use following, special tags: %original_string%, %language_name%, %language_code%, %language_native_name% 46 |
47 | 48 | 49 |
50 |
-------------------------------------------------------------------------------- /menus/settings/auto-translate-strings.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 82 | 83 | 84 |

10 |

scanned for strings.', 'wpml-compatibility-test-tools' ), admin_url( 'admin.php?page=sitepress-multilingual-cms/menu/theme-localization.php' ) ); ?>

11 |
15 |
16 | 21 | 22 | 42 | 43 | 44 | 45 |
46 | 47 |
48 | 49 | 50 |
51 |
52 | 55 |
56 |
57 | 67 |
68 | You can use following, special tags: %original_string%, %language_name%, %language_code%, 69 | %language_native_name% 70 |
71 | 72 | 75 | 78 | 80 |
81 |
85 | -------------------------------------------------------------------------------- /menus/settings/custom-fields-translation.php: -------------------------------------------------------------------------------- 1 | get_custom_fields(); 5 | $translation_preferences = $mt_custom_fields_translation->determine_translation_preference(); 6 | ?> 7 |
8 | 9 |

10 | 11 |
12 |

Custom XML Configuration tab in WPML.', 'wpml-compatibility-test-tools' ), esc_url( admin_url( 'admin.php?page=tm%2Fmenu%2Fsettings&sm=custom-xml-config' ) ) ); ?>

13 |
14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | $translation_preference ): ?> 30 | 31 | 36 | 40 | 45 | 49 | 54 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
32 | 35 | "> 37 | 39 | "> 41 | /> 44 | "> 46 | 48 | "> 50 | /> 53 | 55 | get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = %s LIMIT 1", $custom_field ) ); 59 | 60 | // Define the maximum number of characters to display 61 | $maxLength = 50; 62 | 63 | // If the value length is greater than the defined maximum length, 64 | // cut it down and append '...' to indicate that it's truncated 65 | if ( $value && strlen( $value ) > $maxLength ) { 66 | $value = substr( $value, 0, $maxLength ) . "..."; 67 | } elseif ( empty( $value ) ) { 68 | $value = 'N/A'; 69 | } 70 | 71 | ?> 72 |
87 | 88 | 89 |
90 | 92 | 94 |
95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 |
113 | -------------------------------------------------------------------------------- /menus/settings/generator.php: -------------------------------------------------------------------------------- 1 |
2 |

3 |

4 |
5 | 6 | 7 | 8 | 66 | 67 | 68 | 124 | 125 | 126 | 220 | 221 | 222 | 264 | 265 | 266 | 290 | 291 | 292 | 296 | 297 | 298 | 299 | 300 |
9 | 10 | 11 | 12 | 15 | 18 | 21 | 24 | 25 | 26 | 27 | false ); 28 | 29 | $post_types = get_post_types( $args, 'names' ); 30 | 31 | if ( $post_types ) : 32 | foreach ( $post_types as $post_type ): 33 | $post_type = esc_attr( $post_type ); ?> 34 | 35 | 38 | 41 | 44 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
13 |

14 |
16 | 17 | 19 | 20 | 22 | 23 |
36 | 37 | 39 | /> 40 | 42 | /> 43 | 45 | /> 46 |
Toggle allToggle allToggle allToggle all
65 |
69 | 70 | 71 | 72 | 75 | 78 | 81 | 84 | 85 | 86 | 87 | 92 | 93 | 96 | 99 | 102 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 |
73 |

74 |
76 | 77 | 79 | 80 | 82 | 83 |
94 | 95 | 97 | /> 98 | 100 | /> 101 | 103 | /> 104 |
Toggle allToggle allToggle allToggle all
123 |
127 | 128 | 131 | 132 | 133 | 136 | 139 | 142 | 145 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | meta_key = esc_attr( $custom_field->meta_key ); ?> 161 | 162 | 165 | 168 | 171 | 174 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 192 | 195 | 198 | 201 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 |
134 |

135 |
137 | 138 | 140 | 141 | 143 | 144 | 146 | 147 |
Toggle allToggle allToggle allToggle allToggle all
163 | 164 | "> 166 | meta_key] ) || $_POST['cf'][$custom_field->meta_key] == 'ignore' ) ?>/> 167 | "> 169 | meta_key] ) && $_POST['cf'][$custom_field->meta_key] == 'copy' ) ?>/> 170 | "> 172 | meta_key] ) && $_POST['cf'][$custom_field->meta_key] == 'copy-once' ) ?>/> 173 | "> 175 | meta_key] ) && $_POST['cf'][$custom_field->meta_key] == 'translate' ) ?>/> 176 |
Toggle allToggle allToggle allToggle allToggle all
190 | 191 | 193 | 194 | 196 | 197 | 199 | 200 | 202 | 203 |
219 |
223 | 224 | 225 | 226 | 229 | 230 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 |
227 |

228 |
wp_options table:', 'wpml-compatibility-test-tools' ); ?> 231 | 251 |
Toggle all
263 |
267 | 268 | 269 | 270 | 271 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 |

272 | 273 | 274 | 275 |
Remove all
289 |
293 | 294 | 295 |
301 |
302 |
-------------------------------------------------------------------------------- /menus/settings/overview.php: -------------------------------------------------------------------------------- 1 |
2 |

3 |

4 | 5 | 6 | 7 | 21 | 22 | 23 | 24 | 25 | $config ) : ?> 26 | 27 | 28 | 44 | 45 | 46 | 47 | 48 |
8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

12 |
20 |
29 | 30 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |

33 |
43 |
49 |
50 | -------------------------------------------------------------------------------- /menus/settings/settings.php: -------------------------------------------------------------------------------- 1 |
2 |

3 |

4 | 17 |
-------------------------------------------------------------------------------- /menus/settings/shortcode-helper.php: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 120 | 121 | 122 |

26 |
27 | 28 | 29 |
    30 |
  • 31 | /> 33 |
  • 34 |
35 | 36 | 37 | 38 | 39 |
    40 |
  • 41 | 42 |
      43 |
    1. ', array_keys( $unregistered_tags ) ); ?>
    2. 44 |
    45 | 46 | 47 | 48 |
  • 49 |
50 | 51 | 52 | 53 |
    54 |
  • 55 | 57 |
  • 58 |
59 | 60 | 61 | 62 |
    63 |
  • 64 | 66 |
  • 67 |
68 | 69 | 70 | 71 |
    72 |
  • 73 |
      74 |
    1. ', $default_ignored_tags ); ?>
    2. 75 |
    76 |
  • 77 |
78 | 79 | 80 | 81 |
    82 |
  • 83 | /> 85 |
  • 86 |
87 | 88 | 89 | 90 |
    91 |
  • $values ) { ?> 94 | 95 | 96 |
      $attr_value ) { ?> 97 |
    • {$attr_value}"; ?>
    • 98 |
    99 |
  • 100 | 101 |
102 | 103 | 104 | 105 | 106 | 109 | 110 | 111 | 114 | 115 | 116 | 117 | 118 |
119 |
-------------------------------------------------------------------------------- /multilingual-tools.php: -------------------------------------------------------------------------------- 1 | th, 151 | #wctt-generator thead tr:not(:first-child) > th { 152 | padding: 0 0 3px 10px; 153 | } 154 | .column-desc { 155 | width: 200px; 156 | font-size: 13px !important; 157 | } 158 | #at-toggle { 159 | display: none; 160 | } 161 | #wctt-generator input[name="save"] { 162 | margin: 2px 2px 0 20px; 163 | } 164 | #wctt-generator input[type="submit"] { 165 | float:right; 166 | margin-bottom:5px; 167 | } 168 | #wctt-generator .wctt input[type="text"] { 169 | display: block; 170 | width: 100%; 171 | float: left; 172 | padding: 5px; 173 | } 174 | #save { 175 | padding: 20px; 176 | float: right; 177 | } 178 | .remove { 179 | float: right; 180 | line-height:32px; 181 | width: 10px; 182 | } 183 | #mt-shortcodes .wctt td { 184 | width: 50%; 185 | } 186 | #mt-shortcodes .wctt tr:hover { 187 | background: none !important; 188 | } 189 | #mt-shortcodes tfoot th { 190 | text-align: center; 191 | } 192 | #add-shortcode-button { 193 | float: right; 194 | } 195 | #shortcode-attr-tfield { 196 | width: calc(100% - 22px) !important; 197 | } 198 | ::-webkit-input-placeholder { 199 | color: #BBBBBB; 200 | } 201 | 202 | :-moz-placeholder { /* Firefox 18- */ 203 | color: #BBBBBB; 204 | } 205 | 206 | ::-moz-placeholder { /* Firefox 19+ */ 207 | color: #BBBBBB; 208 | } 209 | 210 | :-ms-input-placeholder { 211 | color: #BBBBBB; 212 | } 213 | #mt-shortcodes .wctt .td-left { 214 | padding: 15px 7px 15px 15px; 215 | } 216 | #mt-shortcodes .wctt .td-right { 217 | padding: 15px 15px 15px 7px; 218 | } 219 | .status { 220 | position: absolute; 221 | padding: 5px 5px 5px 10px; 222 | } 223 | .spinner { 224 | position: relative; 225 | float: right; 226 | background: url('../img/spinner.gif') no-repeat left; 227 | height: 16px; 228 | width: 16px; 229 | margin-top: 2px; 230 | } -------------------------------------------------------------------------------- /res/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnTheGoSystems/multilingual-tools/d2f01dd2b7ab66e1ca7dcc7f72cf758bf41ae500/res/img/spinner.gif -------------------------------------------------------------------------------- /res/img/wctt-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnTheGoSystems/multilingual-tools/d2f01dd2b7ab66e1ca7dcc7f72cf758bf41ae500/res/img/wctt-icon.png -------------------------------------------------------------------------------- /res/img/wctt-multiselect-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnTheGoSystems/multilingual-tools/d2f01dd2b7ab66e1ca7dcc7f72cf758bf41ae500/res/img/wctt-multiselect-bg.png -------------------------------------------------------------------------------- /res/js/mt-script.js: -------------------------------------------------------------------------------- 1 | jQuery(function () { 2 | 3 | var option = [], 4 | result = jQuery('#result'), 5 | submitButton = jQuery('#wctt-generator').find('[type="submit"]'), 6 | dropdownToggle = jQuery('a#strings_auto_translate_context.toggle'), 7 | dropdown = jQuery('#dropdown'), 8 | multiSelect = jQuery('#multiSelect'), 9 | progress = jQuery('span.progress'), 10 | spinner = jQuery('span.spinner'), 11 | submitAutoTranslate = jQuery('#strings_auto_translate_action_translate'), 12 | buttons = jQuery('.button'); 13 | 14 | optionCount(); 15 | buttonToggle(); 16 | 17 | // Toggle drop-down on mouse gesture. 18 | dropdown.click(function (event) { 19 | event.stopPropagation(); 20 | 21 | dropdown.find('ul').slideToggle({ 22 | duration: 50, 23 | start: function () { 24 | dropdownToggle.toggle(); 25 | } 26 | }); 27 | }); 28 | 29 | dropdown.find('ul').on('click', function (event) { 30 | event.stopPropagation(); 31 | }); 32 | 33 | // Update option count on checkbox selection. 34 | dropdown.find('input[type="checkbox"]').change(function () { 35 | optionCount(); 36 | }); 37 | 38 | // Hiding elements if clicked elsewhere 39 | jQuery(document).on('click', function () { 40 | dropdown.find('ul').hide(); 41 | dropdownToggle.hide(); 42 | }); 43 | 44 | // Enable submit button if any checkbox is selected. 45 | jQuery(document).on('click', '[type="checkbox"]', function () { 46 | buttonToggle(); 47 | }); 48 | 49 | multiSelect.find('[type="checkbox"]').click(function () { 50 | 51 | // Collecting options from Multi-Select. 52 | option = multiSelect.find('[type="checkbox"]:checked').map(function (_, i) { 53 | return jQuery(i).val(); 54 | }).get(); 55 | 56 | if (option.length !== 0) { 57 | 58 | var data = { 59 | 'options': option, 60 | 'action': "wpml_ctt_action", 61 | '_wctt_mighty_nonce': jQuery('#_wctt_mighty_nonce').val() 62 | }; 63 | 64 | jQuery.post(mt_data.ajax_url, data, function (response) { 65 | var output, data = JSON.parse(response); 66 | 67 | output = ''; 85 | 86 | jQuery('#tree').remove(); 87 | 88 | var content = jQuery(output).hide(); 89 | result.append(content); 90 | content.fadeIn(); 91 | 92 | jQuery('#at-notice').hide(); 93 | jQuery('tr#at-toggle').show(); 94 | }); 95 | } else { 96 | jQuery('#tree').remove(); 97 | jQuery('#at-notice').fadeIn(); 98 | jQuery('tr#at-toggle').hide(); 99 | } 100 | }); 101 | 102 | // Multi-check options tree. 103 | result.on('click', '[type="checkbox"]', function () { 104 | var current = jQuery(this); 105 | 106 | if (this.checked) { 107 | current.parentsUntil('ul#tree').children('[type="checkbox"]').prop('checked', true); 108 | current.siblings('ul').find('[type="checkbox"]').prop('checked', true); 109 | } else { 110 | current.parent().find('[type="checkbox"]').prop('checked', false); 111 | } 112 | }); 113 | 114 | jQuery("#string_auto_translate_predefined_templates").change(function () { 115 | jQuery("#strings_auto_translate_template").val(jQuery("#string_auto_translate_predefined_templates").find("option:selected").text()); 116 | }); 117 | 118 | jQuery("#duplicate_strings_predefined_templates").change(function () { 119 | jQuery("#duplicate_strings_template").val(jQuery("#duplicate_strings_predefined_templates").find("option:selected").text()); 120 | }); 121 | 122 | // Provides toggle all functionality. 123 | jQuery('.toggle').click(function (event) { 124 | event.preventDefault(); 125 | event.stopPropagation(); 126 | 127 | var group = jQuery('input[id=' + this.id + ']'); 128 | 129 | if (group.attr('type') === 'radio') { 130 | group.prop('checked', true); 131 | jQuery('input[type="checkbox"][id="' + this.id.slice(0, -2) + '"]').prop('checked', true); 132 | buttonToggle(); 133 | } else { 134 | group.prop('checked', !group.prop('checked')); 135 | buttonToggle(); 136 | optionCount(); 137 | } 138 | }); 139 | 140 | // Automatically check checkbox if radio is changed. 141 | jQuery('input[type="radio"]').change(function () { 142 | jQuery(this).closest('tr').find('input[type="checkbox"]').prop('checked', true); 143 | buttonToggle(); 144 | }); 145 | 146 | // Button toggle disable. 147 | function buttonToggle() { 148 | var $nonemptyTextFields = jQuery('input[type=text]').not('[id="shortcode-attr-tfield"]').filter(function () { 149 | return this.value !== '' 150 | }); 151 | 152 | submitButton.attr('disabled', 153 | !jQuery('[type="checkbox"]').not('[class="option"]').is(':checked') && 154 | $nonemptyTextFields.length === 0); 155 | } 156 | 157 | // Count selected strings and options from dropdown. 158 | function optionCount() { 159 | var selectedContexts = dropdown.find('[type="checkbox"]:checked'), 160 | placeholder = jQuery('.placeholder'), 161 | stringsCount = 0, 162 | labels = { 163 | string: 'string', 164 | context: 'context' 165 | }; 166 | 167 | selectedContexts.each(function (_, i) { 168 | stringsCount += parseInt(jQuery(i).parent().text().match(/\((.*)\)/).pop()); 169 | }); 170 | 171 | if (selectedContexts.length > 0) { 172 | placeholder.text('- ' + returnCount(stringsCount, labels.string) + ' selected in ' + returnCount(selectedContexts.length, labels.context) + ' -'); 173 | } else { 174 | placeholder.text('- Select -'); 175 | } 176 | 177 | function returnCount(total, string) { 178 | if (total > 1) { 179 | string += 's'; 180 | } 181 | 182 | return total + ' ' + string; 183 | } 184 | } 185 | 186 | 187 | 188 | /* 189 | * SHORTCODES 190 | */ 191 | 192 | var shortcodes = jQuery('#mt-shortcodes'), 193 | shortcodeNotice = jQuery('#shortcode-notice'), 194 | shortcodeButton = jQuery('#add-shortcode-button'); 195 | 196 | // Add shortcode 197 | 198 | shortcodeButton.click(function (event) { 199 | event.preventDefault(); 200 | 201 | var output = ''; 202 | output += ''; 203 | output += ''; 204 | output += ''; 205 | output += 'X'; 206 | 207 | var content = jQuery(output).hide(); 208 | 209 | shortcodes.find('tbody').append(content); 210 | content.fadeIn(); 211 | shortcodeNotice.hide(); 212 | }); 213 | 214 | // Remove shortcode 215 | 216 | shortcodes.find('.wctt').on('click', '.remove', function (event) { 217 | event.preventDefault(); 218 | 219 | var shortcode = jQuery(this).closest('#mt-shortcode'); 220 | 221 | shortcode.fadeOut(function () { 222 | this.remove(); 223 | }); 224 | 225 | showShortcodeNotice(); 226 | }); 227 | 228 | // Remove all shortcodes 229 | 230 | shortcodes.find('#remove-all').click(function (event) { 231 | event.preventDefault(); 232 | 233 | shortcodes.find('tbody #mt-shortcode').fadeOut(function () { 234 | this.remove(); 235 | }); 236 | 237 | showShortcodeNotice(); 238 | }); 239 | 240 | function showShortcodeNotice() { 241 | shortcodes.find('tbody #mt-shortcode').promise().done(function () { 242 | 243 | if (shortcodes.find('tbody').find('#mt-shortcode').length === 0) { 244 | shortcodeNotice.fadeIn(); 245 | } 246 | 247 | buttonToggle(); 248 | }); 249 | } 250 | 251 | shortcodes.on('keyup', 'input[type=text]', function () { 252 | buttonToggle(); 253 | }); 254 | 255 | /* 256 | * STRINGS AUTO TRANSLATE 257 | */ 258 | submitAutoTranslate.on('click', function () { 259 | const formData = loadData(); 260 | 261 | if (checkData(formData, mt_data.labels) && confirm(mt_data.labels.question)) { 262 | buttons.attr('disabled', true); 263 | generateStringTranslations(formData); 264 | } 265 | }); 266 | 267 | function generateStringTranslations(formData, responseData) { 268 | let data = Object.assign({ 269 | 'action': 'generate_strings_translations_action', 270 | 'contexts': formData.contexts, 271 | 'languages': formData.languages, 272 | 'template': formData.template, 273 | '_mt_mighty_nonce': jQuery('#_mt_mighty_nonce').val() 274 | }, responseData); 275 | 276 | jQuery.post(mt_data.ajax_url, data, function (response) { 277 | progress.text(response.progress + '%'); 278 | spinner.css('visibility', 'visible'); 279 | 280 | if (response === 0) { 281 | responseMsg('Response error.') 282 | return; 283 | } else if (response !== 1) { 284 | generateStringTranslations(formData, response); 285 | } else { 286 | responseMsg('Done.') 287 | } 288 | 289 | function responseMsg(message) { 290 | buttons.attr('disabled', false); 291 | spinner.css('visibility', 'hidden'); 292 | progress.text(message); 293 | setTimeout(function () { 294 | progress.text('') 295 | }, 5000); 296 | } 297 | }); 298 | } 299 | 300 | function loadData() { 301 | return { 302 | languages: jQuery('.active_languages:checkbox:checked').map(function (_, i) { 303 | return jQuery(i).val(); 304 | }).get(), 305 | contexts: jQuery('.strings_auto_translate_context:checkbox:checked').map(function (_, i) { 306 | return jQuery(i).val(); 307 | }).get(), 308 | template: jQuery('#strings_auto_translate_template').val() 309 | }; 310 | } 311 | 312 | function checkData(data, labels) { 313 | let message = ''; 314 | 315 | if (data.contexts.length === 0) { 316 | message += labels.no_context_notice + '\n' 317 | } 318 | 319 | if (data.languages.length === 0) { 320 | message += labels.no_selected_language_notice + '\n' 321 | } 322 | 323 | if (data.template === '') { 324 | message += labels.no_template_notice 325 | } 326 | 327 | if (message !== '') { 328 | alert(message); 329 | return false; 330 | } else 331 | return true; 332 | } 333 | 334 | jQuery('#wpml-cf-form').on('submit', function (e) { 335 | e.preventDefault(); 336 | 337 | var data = jQuery(this).serialize(); 338 | 339 | jQuery.post(ajaxurl, data, function (response) { 340 | // Decode the response before setting it as the textarea value 341 | var decodedResponse = jQuery('
').html(response).text(); 342 | jQuery('#xml-output').text(decodedResponse); 343 | 344 | // Enable the copy button if the textarea is not empty 345 | if (decodedResponse.trim() !== '') { 346 | jQuery('#copy-xml').prop('disabled', false); 347 | } 348 | }); 349 | }); 350 | 351 | jQuery('#copy-xml').on('click', function (e) { 352 | e.preventDefault(); 353 | 354 | var xmlOutput = document.getElementById('xml-output'); 355 | xmlOutput.select(); 356 | 357 | try { 358 | // Copy the selected text to the clipboard 359 | document.execCommand('copy'); 360 | alert('XML copied to clipboard!'); 361 | } catch (err) { 362 | console.log('Oops, unable to copy'); 363 | } 364 | }); 365 | 366 | }); 367 | --------------------------------------------------------------------------------