Skip to content

Commit 2d852f0

Browse files
authored
Custom Fields Translation (#28)
* Custom Fields Translation
1 parent b11d2e8 commit 2d852f0

6 files changed

Lines changed: 326 additions & 3 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ Multilingual Tools are set of tools related to [WPML](https://wpml.org) plugin b
1010
- Auto generate strings for translations
1111
- Add language information to post duplicate
1212
- Generate WPML config file ( wpml-config.xml )
13+
- Assist with Custom Fields translation preferences
1314

1415
## Requirements
1516

1617
For this plugin to work you will need:
1718

1819
- WPML Multilingual CMS
1920
- WPML String Translation
20-
- WPML Translation Management
2121

2222
All plugins can be downloaded from [WPML.org](https://wpml.org)
2323

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php
2+
3+
class MLTools_Custom_Fields_Translation {
4+
public function __construct() {
5+
add_action( 'wp_ajax_wpml_cf_generate_xml', array( $this, 'wpml_cf_generate_xml' ) );
6+
}
7+
8+
public function get_custom_fields() {
9+
global $wpdb;
10+
11+
// We don't need system fields starting with an underscore "_"
12+
13+
$meta_keys = $wpdb->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 = '<wpml-config><custom-fields>';
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 action=\"$preference\">$custom_field</custom-field>";
124+
}
125+
126+
$wpml_config .= '</custom-fields></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+
}

inc/wpml-compatibility-test-tools.class.php

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ public function __construct() {
1010

1111
public function init() {
1212

13+
if (class_exists('MLTools_Custom_Fields_Translation')) {
14+
new MLTools_Custom_Fields_Translation();
15+
}
16+
1317
// Generate XML
1418
if ( isset( $_POST['wctt-generator-submit'] ) && check_admin_referer( 'wctt-generate', '_wctt_mighty_nonce' ) ) {
1519
add_action( 'wp_loaded', array( $this, 'generate_xml' ) );
@@ -318,6 +322,10 @@ public function register_administration_page() {
318322
$this,
319323
'load_template'
320324
) );
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+
) );
321329
}
322330

323331
/**
@@ -341,6 +349,10 @@ public function load_template() {
341349
case 'multilingual-tools_page_mt-generator' :
342350
require WPML_CTT_ABS_PATH . 'menus/settings/generator.php';
343351
break;
352+
353+
case 'multilingual-tools_page_cf-translations' :
354+
require WPML_CTT_ABS_PATH . 'menus/settings/custom-fields-translation.php';
355+
break;
344356
}
345357
}
346358

@@ -369,6 +381,26 @@ public function add_scripts( $hook ) {
369381
) );
370382

371383
}
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+
}
372404
}
373405

374406
/**
@@ -713,4 +745,4 @@ function display_configuration_for_debug( $file ) {
713745
return $file;
714746
}
715747

716-
}
748+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
require_once WPML_CTT_PATH . '/inc/class-mltools-custom-fields-translation.php';
3+
$mt_custom_fields_translation = new MLTools_Custom_Fields_Translation();
4+
$custom_fields = $mt_custom_fields_translation->get_custom_fields();
5+
$translation_preferences = $mt_custom_fields_translation->determine_translation_preference();
6+
?>
7+
<div class="wrap">
8+
9+
<h2><?php _e( 'Custom Field Settings Helper', 'wpml-compatibility-test-tools' ); ?></h2>
10+
11+
<div class="description">
12+
<p><?php echo sprintf( __( 'The following table shows the custom fields with no translation preferences and suggests a translation preference for each field. You can review them and generate the XML. Once the XML is generated, you can copy it and paste it in the <a href="%s">Custom XML Configuration</a> tab in WPML.', 'wpml-compatibility-test-tools' ), esc_url( admin_url( 'admin.php?page=tm%2Fmenu%2Fsettings&sm=custom-xml-config' ) ) ); ?></p>
13+
</div>
14+
15+
<form id="wpml-cf-form">
16+
<table class="widefat">
17+
<?php if ( $custom_fields ): ?>
18+
<thead>
19+
<tr>
20+
<th><?php esc_html_e( 'Custom fields', 'wpml-compatibility-test-tools' ); ?></th>
21+
<th><?php esc_html_e( "Don't translate", 'wpml-compatibility-test-tools' ); ?></th>
22+
<th><?php esc_html_e( "Copy", 'wpml-compatibility-test-tools' ); ?></th>
23+
<th><?php esc_html_e( "Copy once", 'wpml-compatibility-test-tools' ); ?></th>
24+
<th><?php esc_html_e( "Translate", 'wpml-compatibility-test-tools' ); ?></th>
25+
<th><?php esc_html_e( "Sample", 'wpml-compatibility-test-tools' ); ?></th>
26+
</tr>
27+
</thead>
28+
<tbody class="wctt">
29+
<?php foreach ( $translation_preferences as $custom_field => $translation_preference ): ?>
30+
<tr>
31+
<td>
32+
<label>
33+
<?php echo esc_html( $custom_field ); ?>
34+
</label>
35+
</td>
36+
<td title="<?php esc_attr_e( "Don't translate", 'wpml-compatibility-test-tools' ); ?>">
37+
<input id="cf_0_<?php echo esc_attr( $custom_field ); ?>" type="radio"
38+
name="cf[<?php echo esc_attr( $custom_field ); ?>]" value="ignore"/>
39+
</td>
40+
<td title="<?php esc_attr_e( "Copy", 'wpml-compatibility-test-tools' ); ?>">
41+
<input id="cf_1_<?php echo esc_attr( $custom_field ); ?>" type="radio"
42+
name="cf[<?php echo esc_attr( $custom_field ); ?>]"
43+
value="copy" <?php checked( $translation_preference, 'copy' ); ?>/>
44+
</td>
45+
<td title="<?php esc_attr_e( "Copy once", 'wpml-compatibility-test-tools' ); ?>">
46+
<input id="cf_2_<?php echo esc_attr( $custom_field ); ?>" type="radio"
47+
name="cf[<?php echo esc_attr( $custom_field ); ?>]" value="copy-once"/>
48+
</td>
49+
<td title="<?php esc_attr_e( "Translate", 'wpml-compatibility-test-tools' ); ?>">
50+
<input id="cf_3_<?php echo esc_attr( $custom_field ); ?>" type="radio"
51+
name="cf[<?php echo esc_attr( $custom_field ); ?>]"
52+
value="translate" <?php checked( $translation_preference, 'translate' ); ?>/>
53+
</td>
54+
<td>
55+
<?php
56+
global $wpdb;
57+
58+
$value = $wpdb->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+
?><code><?php echo $value; ?></code>
72+
</td>
73+
</tr>
74+
<?php endforeach; ?>
75+
</tbody>
76+
<tfoot>
77+
<tr>
78+
<th><?php esc_html_e( 'Custom fields', 'wpml-compatibility-test-tools' ); ?></th>
79+
<th><?php esc_html_e( "Don't translate", 'wpml-compatibility-test-tools' ); ?></th>
80+
<th><?php esc_html_e( "Copy", 'wpml-compatibility-test-tools' ); ?></th>
81+
<th><?php esc_html_e( "Copy once", 'wpml-compatibility-test-tools' ); ?></th>
82+
<th><?php esc_html_e( "Translate", 'wpml-compatibility-test-tools' ); ?></th>
83+
<th><?php esc_html_e( "Sample", 'wpml-compatibility-test-tools' ); ?></th>
84+
</tr>
85+
</tfoot>
86+
</table>
87+
<?php wp_nonce_field( 'wpml_cf_nonce', 'wpml_cf_nonce' ); ?>
88+
<input type="hidden" name="action" value="wpml_cf_generate_xml"/>
89+
<br>
90+
<button class="button-primary"
91+
type="submit"><?php esc_html_e( 'Generate XML', 'wpml-compatibility-test-tools' ); ?></button>
92+
<button class="button-primary" id="copy-xml" type="button"
93+
disabled><?php esc_html_e( 'Copy XML', 'wpml-compatibility-test-tools' ); ?></button>
94+
</form>
95+
<br>
96+
<textarea id="xml-output" readonly style="width: 90%; min-height: 300px; background-color: #fff"></textarea>
97+
<?php else: ?>
98+
<thead>
99+
<tr>
100+
<th colspan="5"><?php esc_html_e( 'Custom Fields', 'wpml-compatibility-test-tools' ); ?></th>
101+
</tr>
102+
</thead>
103+
<tbody class="wctt">
104+
<tr>
105+
<td colspan="5"><?php esc_html_e( 'It looks like all custom fields have their translation preferences.', 'wpml-compatibility-test-tools' ); ?></td>
106+
</tr>
107+
</tbody>
108+
<?php endif; ?>
109+
</table>
110+
111+
112+
</div>

multilingual-tools.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
require_once WPML_CTT_PATH . '/inc/class-mltools-shortcode-config.php';
2525
require_once WPML_CTT_PATH . '/inc/class-mltools-shortcode-wpml-config-parser.php';
2626
require_once WPML_CTT_PATH . '/inc/class-mltools-xml-helper.php';
27+
require_once WPML_CTT_PATH . '/inc/class-mltools-custom-fields-translation.php';
2728
require_once WPML_CTT_PATH . '/inc/class-mltools-elementor-config-generator.php';
2829

2930
// Disable informations about ICanLocalize.

res/js/mt-script.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,4 +331,36 @@ jQuery(function () {
331331
return true;
332332
}
333333

334-
});
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('<div/>').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+
});

0 commit comments

Comments
 (0)