first commit
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
class WPML_TM_General_Xliff_Import extends WPML_TM_Job_Factory_User {
|
||||
|
||||
/**
|
||||
* @var WPML_TM_Xliff_Reader_Factory $xliff_reader_factory
|
||||
*/
|
||||
private $xliff_reader_factory;
|
||||
|
||||
/**
|
||||
* WPML_TM_General_Xliff_Import constructor.
|
||||
*
|
||||
* @param WPML_Translation_Job_Factory $job_factory
|
||||
* @param WPML_TM_Xliff_Reader_Factory $xliff_reader_factory
|
||||
*/
|
||||
public function __construct( &$job_factory, &$xliff_reader_factory ) {
|
||||
parent::__construct( $job_factory );
|
||||
$this->xliff_reader_factory = &$xliff_reader_factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports the data in the xliff string into an array representation
|
||||
* that fits to the given target translation id.
|
||||
*
|
||||
* @param string $xliff_string
|
||||
* @param int $target_translation_id
|
||||
*
|
||||
* @return WP_Error|array
|
||||
*/
|
||||
public function import( $xliff_string, $target_translation_id ) {
|
||||
$xliff_reader = $this->xliff_reader_factory->general_xliff_reader();
|
||||
$job_data = $xliff_reader->get_data( $xliff_string );
|
||||
if ( is_wp_error( $job_data ) ) {
|
||||
$job = $this->job_factory->job_by_translation_id( $target_translation_id );
|
||||
if ( $job
|
||||
&& ( $id_string = $xliff_reader->get_xliff_job_identifier( $xliff_string ) ) !== false
|
||||
) {
|
||||
$job_data = $xliff_reader->get_data( str_replace( $id_string,
|
||||
$job->get_id() . '-' . md5( $job->get_id() . $job->get_original_element_id() ),
|
||||
$xliff_string ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $job_data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
class WPML_TM_General_Xliff_Reader extends WPML_TM_Xliff_Reader {
|
||||
|
||||
public function get_xliff_job_identifier( $content ) {
|
||||
$xliff = $this->load_xliff( $content );
|
||||
if ( is_wp_error( $xliff ) ) {
|
||||
$identifier = false;
|
||||
} else {
|
||||
$identifier = $this->identifier_from_xliff( $xliff );
|
||||
}
|
||||
|
||||
return $identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the translation from a XLIFF
|
||||
*
|
||||
* @param string $content The XLIFF representing a job
|
||||
*
|
||||
* @return WP_Error|array
|
||||
*/
|
||||
public function get_data( $content ) {
|
||||
$xliff = $this->load_xliff( $content );
|
||||
if ( is_wp_error( $xliff ) ) {
|
||||
$data = $xliff;
|
||||
} else {
|
||||
$job = $this->get_job_for_xliff( $xliff );
|
||||
$data = is_wp_error( $job ) ? $job : $this->generate_job_data( $xliff, $job );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
class WPML_TM_String_Xliff_Reader extends WPML_TM_Xliff_Reader {
|
||||
|
||||
/**
|
||||
* Retrieve the string translations from a XLIFF
|
||||
*
|
||||
* @param string $content The XLIFF representing a set of strings
|
||||
*
|
||||
* @return WP_Error|array The string translation representation or WP_Error
|
||||
* on failure
|
||||
*/
|
||||
public function get_data( $content ) {
|
||||
$xliff = $this->load_xliff( $content );
|
||||
$data = array();
|
||||
if ( $xliff && ! $xliff instanceof WP_Error ) {
|
||||
/** @var SimpleXMLElement $node */
|
||||
foreach ( $xliff->{'file'}->{'body'}->children() as $node ) {
|
||||
$target = $this->get_xliff_node_target( $node );
|
||||
|
||||
if ( ! $target && $target !== '0' ) {
|
||||
return $this->invalid_xliff_error();
|
||||
}
|
||||
$target = $this->replace_xliff_new_line_tag_with_new_line( $target );
|
||||
$attr = $node->attributes();
|
||||
$data[ (string) $attr['id'] ] = $target;
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class WPML_TM_Validate_HTML
|
||||
*/
|
||||
class WPML_TM_Validate_HTML {
|
||||
|
||||
/** @var string Validated html */
|
||||
private $html = '';
|
||||
|
||||
/** @var array Tags currently open */
|
||||
private $tags = array();
|
||||
|
||||
/** @var int Number of errors */
|
||||
private $error_count = 0;
|
||||
|
||||
/**
|
||||
* Get validated html.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_html() {
|
||||
return $this->html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate html.
|
||||
*
|
||||
* @param string $html HTML to process.
|
||||
*
|
||||
* @return int Number of errors.
|
||||
*/
|
||||
public function validate( $html ) {
|
||||
$html = $this->hide_wp_bugs( $html );
|
||||
$html = $this->hide_cdata( $html );
|
||||
$html = $this->hide_comments( $html );
|
||||
$html = $this->hide_self_closing_tags( $html );
|
||||
$html = $this->hide_scripts( $html );
|
||||
$html = $this->hide_styles( $html );
|
||||
|
||||
$processed_html = '';
|
||||
$html_arr = array(
|
||||
'processed' => $processed_html,
|
||||
'next' => $html,
|
||||
);
|
||||
while ( '' !== $html_arr['next'] ) {
|
||||
$html_arr = $this->validate_next( $html_arr['next'] );
|
||||
if ( $html_arr ) {
|
||||
$processed_html .= $html_arr['processed'];
|
||||
}
|
||||
}
|
||||
|
||||
$html = $processed_html;
|
||||
|
||||
$html = $this->restore_styles( $html );
|
||||
$html = $this->restore_scripts( $html );
|
||||
$html = $this->restore_self_closing_tags( $html );
|
||||
$html = $this->restore_comments( $html );
|
||||
$html = $this->restore_wp_bugs( $html );
|
||||
|
||||
$this->html = $html;
|
||||
|
||||
return $this->error_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate first tag in html flow and return processed html and rest.
|
||||
* In processed part broken html is replaced by wpml comment.
|
||||
*
|
||||
* @param string $html HTML to process.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private function validate_next( $html ) {
|
||||
$regs = array();
|
||||
|
||||
// Get first opening or closing tag.
|
||||
$pattern = '<\s*?([a-z]+|/[a-z]+)((?:.|\s)*?)>';
|
||||
mb_eregi( $pattern, $html, $regs );
|
||||
|
||||
if ( $regs ) {
|
||||
$full_tag = $regs[0];
|
||||
$pos = mb_strpos( $html, $full_tag );
|
||||
$next_html = mb_substr( $html, $pos + mb_strlen( $full_tag ) );
|
||||
|
||||
$tag = $regs[1];
|
||||
$result = true;
|
||||
if ( '/' === mb_substr( $tag, 0, 1 ) ) {
|
||||
$result = $this->close_tag( mb_substr( $tag, 1 ) );
|
||||
} else {
|
||||
$this->open_tag( $tag );
|
||||
}
|
||||
|
||||
if ( $result ) {
|
||||
$processed_html = mb_substr( $html, 0, $pos + mb_strlen( $full_tag ) );
|
||||
} else {
|
||||
$processed_html = mb_substr( $html, 0, $pos ) . '<!-- wpml:html_fragment ' . $full_tag . ' -->';
|
||||
}
|
||||
|
||||
return array(
|
||||
'processed' => $processed_html,
|
||||
'next' => $next_html,
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'processed' => $html,
|
||||
'next' => '',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert WP bugs into wpml commented bugs.
|
||||
*
|
||||
* @param string $html HTML to process.
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
private function hide_wp_bugs( $html ) {
|
||||
// WP bug fix for comments - in case you REALLY meant to type '< !--'
|
||||
$html = str_replace( '< !--', '< !--', $html );
|
||||
// WP bug fix for LOVE <3 (and other situations with '<' before a number)
|
||||
$pattern = '<([0-9]{1})';
|
||||
$html = mb_ereg_replace_callback( $pattern, array( $this, 'hide_wp_bug_callback' ), $html, 'msri' );
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to convert WP bugs into wpml commented bugs.
|
||||
*
|
||||
* @param array $matches
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function hide_wp_bug_callback( $matches ) {
|
||||
return '<!-- wpml:wp_bug ' . $matches[0] . ' -->';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert wpml commented bugs to WP bugs.
|
||||
*
|
||||
* @param $html
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
private function restore_wp_bugs( $html ) {
|
||||
$pattern = '<!-- wpml:wp_bug (.*?) -->';
|
||||
$html = mb_ereg_replace_callback( $pattern, array( $this, 'restore_bug_callback' ), $html, 'msri' );
|
||||
$html = str_replace( '< !--', '< !--', $html );
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to convert wpml commented bugs to WP bugs.
|
||||
*
|
||||
* @param array $matches
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function restore_bug_callback( $matches ) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert HTML comments into wpml comments.
|
||||
*
|
||||
* @param string $html HTML to process.
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
private function hide_comments( $html ) {
|
||||
$pattern = '<!--(.*?)-->';
|
||||
$html = mb_ereg_replace_callback( $pattern, array( $this, 'hide_comment_callback' ), $html, 'msri' );
|
||||
|
||||
$pattern = '<!((?!-- wpml:).*?)>';
|
||||
$html = mb_ereg_replace_callback( $pattern, array( $this, 'hide_declaration_callback' ), $html, 'msri' );
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to convert HTML comment to wpml comment.
|
||||
*
|
||||
* @param array $matches
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function hide_comment_callback( $matches ) {
|
||||
return '<!-- wpml:html_comment ' . base64_encode( $matches[0] ) . ' -->';
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to convert HTML declaration to wpml declaration.
|
||||
*
|
||||
* @param array $matches
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function hide_declaration_callback( $matches ) {
|
||||
return '<!-- wpml:html_declaration ' . base64_encode( $matches[0] ) . ' -->';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert wpml comments to HTML comments.
|
||||
*
|
||||
* @param string $html
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function restore_comments( $html ) {
|
||||
$pattern = '<!-- wpml:html_comment (.*?) -->';
|
||||
$html = mb_ereg_replace_callback( $pattern, array( $this, 'restore_encoded_content_callback' ), $html, 'msri' );
|
||||
|
||||
$pattern = '<!-- wpml:html_declaration (.*?) -->';
|
||||
$html = mb_ereg_replace_callback( $pattern, array( $this, 'restore_encoded_content_callback' ), $html, 'msri' );
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to convert wpml base64 encoded content.
|
||||
*
|
||||
* @param array $matches
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function restore_encoded_content_callback( $matches ) {
|
||||
return base64_decode( $matches[1] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert self-closing tags to wpml self-closing tags.
|
||||
*
|
||||
* @param string $html HTML to process.
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
private function hide_self_closing_tags( $html ) {
|
||||
$self_closing_tags = array(
|
||||
'area',
|
||||
'base',
|
||||
'basefont',
|
||||
'br',
|
||||
'col',
|
||||
'command',
|
||||
'embed',
|
||||
'frame',
|
||||
'hr',
|
||||
'img',
|
||||
'input',
|
||||
'isindex',
|
||||
'link',
|
||||
'meta',
|
||||
'param',
|
||||
'source',
|
||||
'track',
|
||||
'wbr',
|
||||
'command',
|
||||
'keygen',
|
||||
'menuitem',
|
||||
'path',
|
||||
'polyline',
|
||||
);
|
||||
foreach ( $self_closing_tags as $self_closing_tag ) {
|
||||
$pattern = '<\s*?' . $self_closing_tag . '((?:.|\s)*?)(>|/>)';
|
||||
$html = mb_ereg_replace_callback( $pattern, array( $this, 'hide_sct_callback' ), $html, 'msri' );
|
||||
}
|
||||
|
||||
$pattern = '<\s*?[^>]*/>';
|
||||
$html = mb_ereg_replace_callback( $pattern, array( $this, 'hide_sct_callback' ), $html, 'msri' );
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to convert self-closing tags to wpml self-closing tags.
|
||||
*
|
||||
* @param $matches
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function hide_sct_callback( $matches ) {
|
||||
return '<!-- wpml:html_self_closing_tag ' . str_replace( array( '<', '>' ), '', $matches[0] ) . ' -->';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert wpml self-closing tags to HTML self-closing tags.
|
||||
*
|
||||
* @param $html
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
private function restore_self_closing_tags( $html ) {
|
||||
$pattern = '<!-- wpml:html_self_closing_tag (.*?) -->';
|
||||
$html = mb_ereg_replace_callback( $pattern, array( $this, 'restore_sct_callback' ), $html, 'msri' );
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to convert wpml self-closing tags to self-closing tags.
|
||||
*
|
||||
* @param $matches
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function restore_sct_callback( $matches ) {
|
||||
return '<' . $matches[1] . '>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert wpml comments to initial HTML.
|
||||
*
|
||||
* @param $html
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
public function restore_html( $html ) {
|
||||
$html = $this->restore_cdata( $html );
|
||||
$html = $this->restore_html_fragments( $html );
|
||||
return $html;
|
||||
}
|
||||
/**
|
||||
* Convert wpml fragments to HTML fragments.
|
||||
*
|
||||
* @param $html
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
private function restore_html_fragments( $html ) {
|
||||
$pattern = '<!-- wpml:html_fragment (.*?) -->';
|
||||
$html = mb_ereg_replace_callback( $pattern, array( $this, 'restore_html_fragment_callback' ), $html, 'msri' );
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to convert wpml fragment to HTML fragment.
|
||||
*
|
||||
* @param $matches
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function restore_html_fragment_callback( $matches ) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert scripts to wpml scripts.
|
||||
*
|
||||
* @param string $html HTML to process.
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
private function hide_scripts( $html ) {
|
||||
$pattern = '<\s*?script\s*?>((?:.|\s)*?)</script\s*?>';
|
||||
$html = mb_ereg_replace_callback( $pattern, array( $this, 'hide_script_callback' ), $html, 'msri' );
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to convert script to wpml script.
|
||||
*
|
||||
* @param array $matches
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function hide_script_callback( $matches ) {
|
||||
return '<!-- wpml:script ' . base64_encode( $matches[0] ) . ' -->';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert wpml scripts to scripts.
|
||||
*
|
||||
* @param $html
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
private function restore_scripts( $html ) {
|
||||
$pattern = '<!-- wpml:script (.*?) -->';
|
||||
$html = mb_ereg_replace_callback( $pattern, array( $this, 'restore_encoded_content_callback' ), $html, 'msri' );
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert CDATA to wpml cdata.
|
||||
*
|
||||
* @param string $html HTML to process.
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
private function hide_cdata( $html ) {
|
||||
$pattern = '<!\[CDATA\[((?:.|\s)*?)\]\]>';
|
||||
$html = mb_ereg_replace_callback( $pattern, array( $this, 'hide_cdata_callback' ), $html, 'msri' );
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to convert CDATA to wpml cdata.
|
||||
*
|
||||
* @param array $matches
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function hide_cdata_callback( $matches ) {
|
||||
return '<!-- wpml:cdata ' . base64_encode( $matches[0] ) . ' -->';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert wpml cdata to CDATA.
|
||||
*
|
||||
* @param $html
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
private function restore_cdata( $html ) {
|
||||
$pattern = '<!-- wpml:cdata (.*?) -->';
|
||||
$html = mb_ereg_replace_callback( $pattern, array( $this, 'restore_encoded_content_callback' ), $html, 'msri' );
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert styles to wpml scripts.
|
||||
*
|
||||
* @param string $html HTML to process.
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
private function hide_styles( $html ) {
|
||||
$pattern = '<\s*?style\s*?>((?:.|\s)*?)</style\s*?>';
|
||||
$html = mb_ereg_replace_callback( $pattern, array( $this, 'hide_style_callback' ), $html, 'msri' );
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to convert style to wpml style.
|
||||
*
|
||||
* @param array $matches
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function hide_style_callback( $matches ) {
|
||||
return '<!-- wpml:style ' . base64_encode( $matches[0] ) . ' -->';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert wpml styles to styles.
|
||||
*
|
||||
* @param $html
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
private function restore_styles( $html ) {
|
||||
$pattern = '<!-- wpml:style (.*?) -->';
|
||||
$html = mb_ereg_replace_callback( $pattern, array( $this, 'restore_encoded_content_callback' ), $html, 'msri' );
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open tag encountered in html.
|
||||
*
|
||||
* @param string $tag Tag name.
|
||||
*/
|
||||
private function open_tag( $tag ) {
|
||||
$tag = mb_strtolower( $tag );
|
||||
array_push( $this->tags, $tag );
|
||||
}
|
||||
|
||||
/**
|
||||
* Close tag encountered in html.
|
||||
*
|
||||
* @param string $tag Tag name.
|
||||
*
|
||||
* @return bool Closed successfully.
|
||||
*/
|
||||
private function close_tag( $tag ) {
|
||||
$tag = mb_strtolower( $tag );
|
||||
$last_tag = end( $this->tags );
|
||||
if ( $last_tag === $tag ) {
|
||||
array_pop( $this->tags );
|
||||
|
||||
return true;
|
||||
} else {
|
||||
$this->error_count ++;
|
||||
if ( in_array( $tag, $this->tags, true ) ) {
|
||||
do {
|
||||
array_pop( $this->tags );
|
||||
} while ( $this->tags && end( $this->tags ) !== $tag );
|
||||
array_pop( $this->tags );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @author OnTheGo Systems
|
||||
*/
|
||||
class WPML_TM_XLIFF_Factory {
|
||||
const WPML_XLIFF_DEFAULT_VERSION = WPML_XLIFF_DEFAULT_VERSION;
|
||||
const CREATE_FOR_WRITE = 'WPML_TM_Xliff_Writer';
|
||||
const CREATE_FOR_FRONT_END = 'WPML_TM_Xliff_Frontend';
|
||||
|
||||
public function create_writer( $xliff_version = self::WPML_XLIFF_DEFAULT_VERSION ) {
|
||||
return new WPML_TM_Xliff_Writer( wpml_tm_load_job_factory(), $xliff_version, wpml_tm_xliff_shortcodes() );
|
||||
}
|
||||
|
||||
public function create_frontend() {
|
||||
global $sitepress;
|
||||
$support_info = new WPML_TM_Support_Info();
|
||||
|
||||
return new WPML_TM_Xliff_Frontend( wpml_tm_load_job_factory(), $sitepress, $support_info->is_simplexml_extension_loaded() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,785 @@
|
||||
<?php
|
||||
/**
|
||||
* WPML_TM_Xliff_Frontend class file
|
||||
*
|
||||
* @package wpml-translation-management
|
||||
*/
|
||||
|
||||
use WPML\FP\Obj;
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
require_once WPML_TM_PATH . '/inc/wpml_zip.php';
|
||||
|
||||
/**
|
||||
* Class WPML_TM_Xliff_Frontend
|
||||
*/
|
||||
class WPML_TM_Xliff_Frontend extends WPML_TM_Xliff_Shared {
|
||||
|
||||
/**
|
||||
* Success admin notices
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $success;
|
||||
|
||||
/**
|
||||
* Attachments
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $attachments = array();
|
||||
|
||||
/**
|
||||
* SitePress instance
|
||||
*
|
||||
* @var SitePress
|
||||
*/
|
||||
private $sitepress;
|
||||
|
||||
/**
|
||||
* Name of archive
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $export_archive_name;
|
||||
|
||||
/**
|
||||
* Priority of late initialisation
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $late_init_priority = 9999;
|
||||
|
||||
/**
|
||||
* Is simple xml turned on
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $simplexml_on;
|
||||
|
||||
/**
|
||||
* WPML_TM_Xliff_Frontend constructor
|
||||
*
|
||||
* @param WPML_Translation_Job_Factory $job_factory Job factory.
|
||||
* @param SitePress $sitepress SitePress instance.
|
||||
* @param boolean $simplexml_on Is simple xml turned on.
|
||||
*/
|
||||
public function __construct( WPML_Translation_Job_Factory $job_factory, SitePress $sitepress, $simplexml_on ) {
|
||||
parent::__construct( $job_factory );
|
||||
$this->sitepress = $sitepress;
|
||||
$this->simplexml_on = $simplexml_on;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available xliff versions
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_available_xliff_versions() {
|
||||
|
||||
return array(
|
||||
'10' => '1.0',
|
||||
'11' => '1.1',
|
||||
'12' => '1.2',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get init priority
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_init_priority() {
|
||||
return isset( $_POST['xliff_upload'] ) ||
|
||||
( isset( $_GET['wpml_xliff_action'] ) && $_GET['wpml_xliff_action'] === 'download' ) ||
|
||||
isset( $_POST['wpml_xliff_export_all_filtered'] ) ?
|
||||
$this->get_late_init_priority() : 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get late init priority
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_late_init_priority() {
|
||||
return $this->late_init_priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init class
|
||||
*
|
||||
* @return bool
|
||||
* @throws Exception Throws an exception in case of errors.
|
||||
*/
|
||||
public function init() {
|
||||
$this->attachments = array();
|
||||
$this->error = null;
|
||||
if ( $this->sitepress->get_wp_api()->is_admin() ) {
|
||||
add_action( 'admin_head', array( $this, 'js_scripts' ) );
|
||||
add_action(
|
||||
'wp_ajax_set_xliff_options',
|
||||
array(
|
||||
$this,
|
||||
'ajax_set_xliff_options',
|
||||
),
|
||||
10,
|
||||
2
|
||||
);
|
||||
if ( ! $this->sitepress->get_setting( 'xliff_newlines' ) ) {
|
||||
$this->sitepress->set_setting( 'xliff_newlines', WPML_XLIFF_TM_NEWLINES_ORIGINAL, true );
|
||||
}
|
||||
if ( ! $this->sitepress->get_setting( 'tm_xliff_version' ) ) {
|
||||
$this->sitepress->set_setting( 'tm_xliff_version', '12', true );
|
||||
}
|
||||
|
||||
if ( 1 < count( $this->sitepress->get_languages( false, true ) ) ) {
|
||||
add_filter(
|
||||
'wpml_translation_queue_actions',
|
||||
array(
|
||||
$this,
|
||||
'translation_queue_add_actions',
|
||||
)
|
||||
);
|
||||
add_action(
|
||||
'wpml_xliff_select_actions',
|
||||
array(
|
||||
$this,
|
||||
'translation_queue_xliff_select_actions',
|
||||
),
|
||||
10,
|
||||
4
|
||||
);
|
||||
add_action(
|
||||
'wpml_translation_queue_after_display',
|
||||
array(
|
||||
$this,
|
||||
'translation_queue_after_display',
|
||||
),
|
||||
10,
|
||||
2
|
||||
);
|
||||
add_action(
|
||||
'wpml_translator_notification',
|
||||
array(
|
||||
$this,
|
||||
'translator_notification',
|
||||
),
|
||||
10,
|
||||
0
|
||||
);
|
||||
add_filter(
|
||||
'wpml_new_job_notification',
|
||||
array(
|
||||
$this,
|
||||
'new_job_notification',
|
||||
),
|
||||
10,
|
||||
2
|
||||
);
|
||||
add_filter(
|
||||
'wpml_new_job_notification_attachments',
|
||||
array(
|
||||
$this,
|
||||
'new_job_notification_attachments',
|
||||
)
|
||||
);
|
||||
}
|
||||
if (
|
||||
isset( $_GET['wpml_xliff_action'] ) &&
|
||||
$_GET['wpml_xliff_action'] === 'download' &&
|
||||
wp_verify_nonce( $_GET['nonce'], 'xliff-export' )
|
||||
) {
|
||||
$archive = $this->get_xliff_archive(
|
||||
Obj::propOr( '', 'xliff_version', $_GET ),
|
||||
explode( ',', Obj::propOr( '', 'jobs', $_GET ) )
|
||||
);
|
||||
$this->stream_xliff_archive( $archive );
|
||||
}
|
||||
|
||||
add_action( 'wp_ajax_wpml_xliff_upload', function () {
|
||||
if ( $this->import_xliff( Obj::propOr( [], 'import', $_FILES ) ) ) {
|
||||
wp_send_json_success( $this->success );
|
||||
} else {
|
||||
wp_send_json_error( $this->error );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set xliff options
|
||||
*/
|
||||
public function ajax_set_xliff_options() {
|
||||
check_ajax_referer( 'icl_xliff_options_form_nonce', 'security' );
|
||||
$newlines = isset( $_POST['icl_xliff_newlines'] ) ? (int) $_POST['icl_xliff_newlines'] : 0;
|
||||
$this->sitepress->set_setting( 'xliff_newlines', $newlines, true );
|
||||
$version = isset( $_POST['icl_xliff_version'] ) ? (int) $_POST['icl_xliff_version'] : 0;
|
||||
$this->sitepress->set_setting( 'tm_xliff_version', $version, true );
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'message' => 'OK',
|
||||
'newlines_saved' => $newlines,
|
||||
'version_saved' => $version,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* New job notification
|
||||
*
|
||||
* @param array $mail Email content.
|
||||
* @param int $job_id Job id.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function new_job_notification( $mail, $job_id ) {
|
||||
$tm_settings = $this->sitepress->get_setting( 'translation-management', array() );
|
||||
|
||||
if ( isset( $tm_settings['notification']['include_xliff'] ) && $tm_settings['notification']['include_xliff'] ) {
|
||||
$xliff_version = $this->get_user_xliff_version();
|
||||
$xliff_file = $this->get_xliff_file( $job_id, $xliff_version );
|
||||
$temp_dir = get_temp_dir();
|
||||
$file_name = $temp_dir . get_bloginfo( 'name' ) . '-translation-job-' . $job_id . '.xliff';
|
||||
$fh = fopen( $file_name, 'w' );
|
||||
if ( $fh ) {
|
||||
fwrite( $fh, $xliff_file );
|
||||
fclose( $fh );
|
||||
$mail['attachment'] = $file_name;
|
||||
$this->attachments[ $job_id ] = $file_name;
|
||||
|
||||
$mail['body'] .= __( ' - A xliff file is attached.', 'wpml-translation-management' );
|
||||
}
|
||||
}
|
||||
|
||||
return $mail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get zip name from jobs
|
||||
*
|
||||
* @param array $job_ids Job ids.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_zip_name_from_jobs( $job_ids ) {
|
||||
$min_job = min( $job_ids );
|
||||
$max_job = max( $job_ids );
|
||||
if ( $max_job === $min_job ) {
|
||||
return get_bloginfo( 'name' ) . '-translation-job-' . $max_job . '.zip';
|
||||
} else {
|
||||
return get_bloginfo( 'name' ) . '-translation-job-' . $min_job . '-' . $max_job . '.zip';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* New job notification attachments
|
||||
*
|
||||
* @param array $attachments Job notification attachments.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function new_job_notification_attachments( $attachments ) {
|
||||
$found = false;
|
||||
$archive = new wpml_zip();
|
||||
|
||||
foreach ( $attachments as $index => $attachment ) {
|
||||
if ( in_array( $attachment, $this->attachments, true ) ) {
|
||||
$fh = fopen( $attachment, 'r' );
|
||||
$xliff_file = fread( $fh, filesize( $attachment ) );
|
||||
fclose( $fh );
|
||||
$archive->addFile( $xliff_file, basename( $attachment ) );
|
||||
|
||||
unset( $attachments[ $index ] );
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $found ) {
|
||||
// Add the zip file to the attachments.
|
||||
$archive_data = $archive->getZipData();
|
||||
$temp_dir = get_temp_dir();
|
||||
$file_name = $temp_dir . $this->get_zip_name_from_jobs( array_keys( $this->attachments ) );
|
||||
$fh = fopen( $file_name, 'w' );
|
||||
fwrite( $fh, $archive_data );
|
||||
fclose( $fh );
|
||||
$attachments[] = $file_name;
|
||||
}
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get xliff file
|
||||
*
|
||||
* @param int $job_id Job id.
|
||||
* @param string $xliff_version Xliff version.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_xliff_file( $job_id, $xliff_version = WPML_XLIFF_DEFAULT_VERSION ) {
|
||||
return wpml_tm_xliff_factory()
|
||||
->create_writer( $xliff_version )
|
||||
->generate_job_xliff( $job_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get xliff archive
|
||||
*
|
||||
* @param string $xliff_version Xliff version.
|
||||
* @param array|null $job_ids Job ids.
|
||||
*
|
||||
* @return wpml_zip
|
||||
*
|
||||
* @throws Exception Throws an exception in case of errors.
|
||||
*/
|
||||
public function get_xliff_archive( $xliff_version, $job_ids = array() ) {
|
||||
global $wpdb, $current_user;
|
||||
|
||||
if ( empty( $job_ids ) && isset( $_GET['xliff_export_data'] ) ) {
|
||||
// phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||
$data = json_decode( base64_decode( $_GET['xliff_export_data'] ) );
|
||||
// phpcs:enable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||
$job_ids = isset( $data->job ) ? array_keys( (array) $data->job ) : array();
|
||||
}
|
||||
|
||||
$archive = new wpml_zip();
|
||||
foreach ( $job_ids as $job_id ) {
|
||||
$xliff_file = $this->get_xliff_file( $job_id, $xliff_version );
|
||||
|
||||
// Assign the job to this translator.
|
||||
$rid = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT rid
|
||||
FROM {$wpdb->prefix}icl_translate_job
|
||||
WHERE job_id=%d ",
|
||||
$job_id
|
||||
)
|
||||
);
|
||||
$data_value = array( 'translator_id' => $current_user->ID );
|
||||
$data_where = array( 'job_id' => $job_id );
|
||||
$wpdb->update( $wpdb->prefix . 'icl_translate_job', $data_value, $data_where );
|
||||
$data_where = array( 'rid' => $rid );
|
||||
$wpdb->update( $wpdb->prefix . 'icl_translation_status', $data_value, $data_where );
|
||||
$archive->addFile( $xliff_file, get_bloginfo( 'name' ) . '-translation-job-' . $job_id . '.xliff' );
|
||||
}
|
||||
|
||||
$this->export_archive_name = $this->get_zip_name_from_jobs( $job_ids );
|
||||
$archive->finalize();
|
||||
|
||||
return $archive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream xliff archive
|
||||
*
|
||||
* @param wpml_zip $archive Zip archive.
|
||||
*
|
||||
* @throws Exception Throws an exception in case of errors.
|
||||
*/
|
||||
private function stream_xliff_archive( $archive ) {
|
||||
if ( is_a( $archive, 'wpml_zip' ) ) {
|
||||
if ( defined( 'WPML_SAVE_XLIFF_PATH' ) && trim( WPML_SAVE_XLIFF_PATH ) ) {
|
||||
$this->save_zip_file( WPML_SAVE_XLIFF_PATH, $archive );
|
||||
}
|
||||
$archive->sendZip( $this->export_archive_name );
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save zip file
|
||||
*
|
||||
* @param string $path Where to save the archive.
|
||||
* @param wpml_zip $archive Zip archive.
|
||||
*/
|
||||
private function save_zip_file( $path, $archive ) {
|
||||
$path = trailingslashit( $path );
|
||||
if ( ! is_dir( $path ) ) {
|
||||
$result = mkdir( $path );
|
||||
if ( ! $result ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$archive->setZipFile( $path . $this->export_archive_name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops any redirects from happening when we call the
|
||||
* translation manager to save the translations.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function stop_redirect() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import xliff file
|
||||
*
|
||||
* @param array $file Xliff file data.
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
private function import_xliff( $file ) {
|
||||
global $current_user;
|
||||
|
||||
// We don't want any redirects happening when we save the translation.
|
||||
add_filter( 'wp_redirect', array( $this, 'stop_redirect' ) );
|
||||
|
||||
$this->success = array();
|
||||
$contents = array();
|
||||
|
||||
if ( 0 === (int) $file['size'] ) {
|
||||
$this->error = new WP_Error( 'empty_file', __( 'You are trying to import an empty file.', 'wpml-translation-management' ) );
|
||||
|
||||
return false;
|
||||
} elseif ( isset( $file['tmp_name'] ) && $file['tmp_name'] ) {
|
||||
$fh = fopen( $file['tmp_name'], 'r' );
|
||||
$data = fread( $fh, 4 );
|
||||
fclose( $fh );
|
||||
if ( $data[0] == 'P' && $data[1] == 'K' && $data[2] == chr( 03 ) && $data[3] == chr( 04 ) ) {
|
||||
if ( class_exists( 'ZipArchive' ) ) {
|
||||
$z = new ZipArchive();
|
||||
$zopen = $z->open( $file['tmp_name'], 4 );
|
||||
if ( true !== $zopen ) {
|
||||
$this->error = new WP_Error( 'incompatible_archive', __( 'Incompatible Archive.', 'wpml-translation-management' ) );
|
||||
|
||||
return false;
|
||||
}
|
||||
$empty_files = array();
|
||||
for ( $i = 0; $i < $z->numFiles; $i ++ ) {
|
||||
if ( ! $info = $z->statIndex( $i ) ) {
|
||||
$this->error = new WP_Error( 'stat_failed', __( 'Could not retrieve file from archive.', 'wpml-translation-management' ) );
|
||||
|
||||
return false;
|
||||
}
|
||||
$content = $z->getFromIndex( $i );
|
||||
if ( false === (bool) $content ) {
|
||||
$empty_files[] = $info['name'];
|
||||
}
|
||||
$contents[ $info['name'] ] = $content;
|
||||
}
|
||||
if ( $empty_files ) {
|
||||
$this->error = new WP_Error( 'extract_failed', __( 'The archive contains one or more empty files.', 'wpml-translation-management' ), $empty_files );
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
require_once ABSPATH . 'wp-admin/includes/class-pclzip.php';
|
||||
$archive = new PclZip( $file['tmp_name'] );
|
||||
// Is the archive valid?
|
||||
$archive_files = $archive->extract( PCLZIP_OPT_EXTRACT_AS_STRING );
|
||||
if ( false == $archive_files ) {
|
||||
$this->error = new WP_Error( 'incompatible_archive', __( 'You are trying to import an incompatible Archive.', 'wpml-translation-management' ), $archive->errorInfo( true ) );
|
||||
|
||||
return false;
|
||||
}
|
||||
if ( 0 === count( $archive_files ) ) {
|
||||
$this->error = new WP_Error( 'empty_archive', __( 'You are trying to import an empty archive.', 'wpml-translation-management' ) );
|
||||
|
||||
return false;
|
||||
}
|
||||
$empty_files = array();
|
||||
foreach ( $archive_files as $content ) {
|
||||
if ( false === (bool) $content['content'] ) {
|
||||
$empty_files[] = $content['filename'];
|
||||
}
|
||||
$contents[ $content['filename'] ] = $content['content'];
|
||||
}
|
||||
if ( $empty_files ) {
|
||||
$this->error = new WP_Error( 'extract_failed', __( 'The archive contains one or more empty files.', 'wpml-translation-management' ), $empty_files );
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$fh = fopen( $file['tmp_name'], 'r' );
|
||||
$data = fread( $fh, $file['size'] );
|
||||
fclose( $fh );
|
||||
$contents[ $file['name'] ] = $data;
|
||||
}
|
||||
|
||||
foreach ( $contents as $name => $content ) {
|
||||
if ( $this->validate_file_name( $name ) ) {
|
||||
list( $job, $job_data ) = $this->validate_file( $name, $content, $current_user );
|
||||
if ( null !== $this->error ) {
|
||||
return $job_data;
|
||||
}
|
||||
kses_remove_filters();
|
||||
wpml_tm_save_data( $job_data );
|
||||
kses_init();
|
||||
// translators: %s: job id.
|
||||
$this->success[] = $job->job_id;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translation queue actions
|
||||
*
|
||||
* @param array $actions Actions.
|
||||
* @param string $action_name Action name.
|
||||
* @param array $translation_jobs Translation jobs.
|
||||
* @param string $action
|
||||
*/
|
||||
public function translation_queue_xliff_select_actions( $actions, $action_name, $translation_jobs, $action ) {
|
||||
if ( $this->has_translation_jobs( $translation_jobs ) && sizeof( $actions ) > 0 ) :
|
||||
$currentAction = $action ?: false;
|
||||
?>
|
||||
<div class="alignleft actions">
|
||||
<select name="<?php echo esc_html( $action_name ); ?>">
|
||||
<option
|
||||
value="-1" <?php echo $currentAction == false ? "selected='selected'" : ''; ?>><?php _e( 'Bulk Actions' ); ?></option>
|
||||
<?php foreach ( $actions as $key => $action ) : ?>
|
||||
<option <?php disabled( ! $this->simplexml_on ); ?>
|
||||
value="<?php echo $key; ?>" <?php echo $currentAction == $key && $this->simplexml_on ? "selected='selected'" : ''; ?>><?php echo $action; ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<input
|
||||
id="js-wpml-do-action"
|
||||
type="submit" value="<?php esc_attr_e( 'Apply' ); ?>"
|
||||
name="do<?php echo esc_html( $action_name ); ?>"
|
||||
class="button-secondary action"/>
|
||||
</div>
|
||||
<?php
|
||||
endif;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has translation jobs
|
||||
*
|
||||
* @param array $translation_jobs Translation jobs.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function has_translation_jobs( $translation_jobs ) {
|
||||
return $translation_jobs && array_key_exists( 'jobs', $translation_jobs ) && $translation_jobs['jobs'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get xliff version select options
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_xliff_version_select_options() {
|
||||
$output = '';
|
||||
$user_version = (int) $this->get_user_xliff_version();
|
||||
foreach ( $this->get_available_xliff_versions() as $value => $label ) {
|
||||
$user_version = false === $user_version ? $value : $user_version;
|
||||
|
||||
$output .= '<option value="' . $value . '"';
|
||||
$output .= $user_version === $value ? 'selected="selected"' : '';
|
||||
$output .= '>XLIFF ' . $label . '</option>';
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the various possible XLIFF versions to translations queue page's export actions on display
|
||||
*
|
||||
* @param array $actions Actions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function translation_queue_add_actions( $actions ) {
|
||||
foreach ( $this->get_available_xliff_versions() as $key => $value ) {
|
||||
// translators: %s: XLIFF version.
|
||||
$actions[ $key ] = sprintf( __( 'Export XLIFF %s', 'wpml-translation-management' ), $value );
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error messages in admin notices
|
||||
*/
|
||||
public function admin_notices_error() {
|
||||
if ( is_wp_error( $this->error ) ) {
|
||||
?>
|
||||
<div class="message error">
|
||||
<p><?php echo $this->error->get_error_message(); ?></p>
|
||||
<?php
|
||||
if ( $this->error->get_error_data() ) {
|
||||
?>
|
||||
<ol>
|
||||
<li><?php echo implode( '</li><li>', $this->error->get_error_data() ); ?></li>
|
||||
</ol>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show success messages in admin notices
|
||||
*/
|
||||
public function admin_notices_success() {
|
||||
?>
|
||||
<div class="message updated"><p>
|
||||
<ul>
|
||||
<?php
|
||||
foreach ( $this->success as $message ) {
|
||||
echo '<li>' . $message . '</li>';
|
||||
}
|
||||
?>
|
||||
</ul>
|
||||
</p></div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Check translation queue after display
|
||||
*
|
||||
* @param array $translation_jobs Translation jobs.
|
||||
*/
|
||||
public function translation_queue_after_display( $translation_jobs = array() ) {
|
||||
if ( ! $this->has_translation_jobs( $translation_jobs ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$export_label = esc_html__( 'Export all jobs:', 'wpml-translation-management' );
|
||||
|
||||
$cookie_filters = WPML_Translations_Queue::get_cookie_filters();
|
||||
|
||||
if ( $cookie_filters ) {
|
||||
$type = __( 'All types', 'wpml-translation-management' );
|
||||
|
||||
if ( ! empty( $cookie_filters['type'] ) ) {
|
||||
$post_slug = preg_replace( '/^post_|^package_/', '', $cookie_filters['type'], 1 );
|
||||
$post_types = $this->sitepress->get_translatable_documents( true );
|
||||
$post_types = apply_filters( 'wpml_get_translatable_types', $post_types );
|
||||
|
||||
if ( array_key_exists( $post_slug, $post_types ) ) {
|
||||
$type = $post_types[ $post_slug ]->label;
|
||||
}
|
||||
}
|
||||
|
||||
$from = ! empty( $cookie_filters['from'] )
|
||||
? $this->sitepress->get_display_language_name( $cookie_filters['from'] )
|
||||
: __( 'Any language', 'wpml-translation-management' );
|
||||
$to = ! empty( $cookie_filters['to'] )
|
||||
? $this->sitepress->get_display_language_name( $cookie_filters['to'] )
|
||||
: __( 'Any language', 'wpml-translation-management' );
|
||||
|
||||
$status = ! empty( $cookie_filters['status'] ) && (int) $cookie_filters['status'] !== ICL_TM_WAITING_FOR_TRANSLATOR
|
||||
? TranslationManagement::status2text( $cookie_filters['status'] )
|
||||
: ( ! empty( $cookie_filters['status'] ) ? __( 'Available to translate', 'wpml-translation-management' ) : 'All statuses' );
|
||||
|
||||
$export_label = sprintf(
|
||||
// translators: %1: post type, %2: from language, %3: to language, %4: status.
|
||||
esc_html__( 'Export all filtered jobs of %1$s from %2$s to %3$s in %4$s:', 'wpml-translation-management' ),
|
||||
'<b>' . $type . '</b>',
|
||||
'<b>' . $from . '</b>',
|
||||
'<b>' . $to . '</b>',
|
||||
'<b>' . $status . '</b>'
|
||||
);
|
||||
}
|
||||
?>
|
||||
|
||||
<br/>
|
||||
<table class="widefat">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Import / Export XLIFF', 'wpml-translation-management' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<?php if ( ! $this->simplexml_on ) : ?>
|
||||
<div class="otgs-notice error">
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'SimpleXML missing!', 'wpml-translation-management' ); ?></strong>
|
||||
</p>
|
||||
<p>
|
||||
<?php esc_html_e( 'SimpleXML extension is required for using XLIFF files in WPML Translation Management.', 'wpml-translation-management' ); ?>
|
||||
<a href="https://wpml.org/?page_id=716"><?php esc_html_e( 'WPML Minimum Requirements', 'wpml-translation-management' ); ?></a>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<form method="post" id="translation-xliff-export-all-filtered" action="">
|
||||
<label for="wpml_xliff_export_all_filtered"><?php echo $export_label; ?></label>
|
||||
<select name="xliff_version" class="select" <?php disabled( ! $this->simplexml_on ); ?>>
|
||||
<?php
|
||||
echo $this->get_xliff_version_select_options();
|
||||
?>
|
||||
</select>
|
||||
<input
|
||||
type="submit"
|
||||
value="<?php esc_attr_e( 'Export', 'wpml-translation-management' ); ?>"
|
||||
<?php
|
||||
disabled( ! $this->simplexml_on );
|
||||
?>
|
||||
name="wpml_xliff_export_all_filtered" id="xliff_download"
|
||||
class="button-secondary action"/>
|
||||
<input
|
||||
type="hidden" value="<?php echo wp_create_nonce( 'xliff-export-all-filtered' ); ?>"
|
||||
name="nonce">
|
||||
</form>
|
||||
<hr>
|
||||
<form enctype="multipart/form-data" method="post" id="translation-xliff-upload" action="">
|
||||
<label for="upload-xliff-file"><?php _e( 'Select the xliff file or zip file to upload from your computer: ', 'wpml-translation-management' ); ?></label>
|
||||
<input
|
||||
type="file" id="upload-xliff-file"
|
||||
name="import" <?php disabled( ! $this->simplexml_on ); ?> />
|
||||
<input
|
||||
type="submit" value="<?php _e( 'Upload', 'wpml-translation-management' ); ?>"
|
||||
name="xliff_upload" id="xliff_upload" class="button-secondary action"
|
||||
<?php
|
||||
disabled( ! $this->simplexml_on );
|
||||
?>
|
||||
/>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Print online js script
|
||||
*/
|
||||
public function js_scripts() {
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
var wpml_xliff_ajax_nonce = '<?php echo wp_create_nonce( 'icl_xliff_options_form_nonce' ); ?>';
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide translator notification
|
||||
*/
|
||||
public function translator_notification() {
|
||||
$checked = $this->sitepress->get_setting( 'include_xliff_in_notification' ) ? 'checked="checked"' : '';
|
||||
?>
|
||||
<input
|
||||
type="checkbox" name="include_xliff" id="icl_include_xliff"
|
||||
value="1" <?php echo $checked; ?>/>
|
||||
<label
|
||||
for="icl_include_xliff"><?php _e( 'Include XLIFF files in notification emails', 'wpml-translation-management' ); ?></label>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user xliff version
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
private function get_user_xliff_version() {
|
||||
|
||||
return $this->sitepress->get_setting( 'tm_xliff_version', false );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* @author OnTheGo Systems
|
||||
*/
|
||||
class WPML_TM_XLIFF_Post_Type extends WPML_TM_XLIFF_Phase {
|
||||
|
||||
private $post_type;
|
||||
|
||||
public function __construct( $post_type = '' ) {
|
||||
$this->post_type = $post_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function get_data() {
|
||||
return $this->post_type;
|
||||
}
|
||||
|
||||
protected function get_phase_name() {
|
||||
return 'post_type';
|
||||
}
|
||||
|
||||
protected function get_process_name() {
|
||||
return 'Post type';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
class WPML_TM_Xliff_Reader_Factory extends WPML_TM_Job_Factory_User {
|
||||
|
||||
/**
|
||||
* @return WPML_TM_General_Xliff_Reader
|
||||
*/
|
||||
public function general_xliff_reader() {
|
||||
|
||||
return new WPML_TM_General_Xliff_Reader( $this->job_factory );
|
||||
}
|
||||
|
||||
public function general_xliff_import() {
|
||||
|
||||
return new WPML_TM_General_Xliff_Import( $this->job_factory, $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WPML_TM_String_Xliff_Reader
|
||||
*/
|
||||
public function string_xliff_reader() {
|
||||
|
||||
return new WPML_TM_String_Xliff_Reader( $this->job_factory );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
abstract class WPML_TM_Xliff_Reader extends WPML_TM_Xliff_Shared {
|
||||
|
||||
/**
|
||||
* @param string $content Xliff file string content
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract public function get_data( $content );
|
||||
|
||||
/**
|
||||
* Parse a XML containing the XLIFF
|
||||
*
|
||||
* @param string $content
|
||||
*
|
||||
* @return SimpleXMLElement|WP_Error The parsed XLIFF or a WP error in case it could not be parsed
|
||||
*/
|
||||
public function load_xliff( $content ) {
|
||||
try {
|
||||
$xml = simplexml_load_string( $content );
|
||||
} catch ( Exception $e ) {
|
||||
$xml = false;
|
||||
}
|
||||
|
||||
return $xml ? $xml
|
||||
: new WP_Error(
|
||||
'not_xml_file',
|
||||
sprintf(
|
||||
__( 'The xliff file could not be read.', 'wpml-translation-management' )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
abstract class WPML_TM_Xliff_Shared extends WPML_TM_Job_Factory_User {
|
||||
/** @var WP_Error $error */
|
||||
protected $error;
|
||||
|
||||
/** @var WPML_TM_Validate_HTML */
|
||||
private $validator = null;
|
||||
|
||||
/**
|
||||
* @param $string
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function replace_xliff_new_line_tag_with_new_line( $string ) {
|
||||
return WPML_TP_Xliff_Parser::restore_new_line( $string );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SimpleXMLElement $xliff
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function identifier_from_xliff( $xliff ) {
|
||||
$file_attributes = $xliff->{'file'}->attributes();
|
||||
|
||||
return (string) $file_attributes['original'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SimpleXMLElement $xliff
|
||||
*
|
||||
* @return stdClass|WP_Error
|
||||
*/
|
||||
public function get_job_for_xliff( SimpleXMLElement $xliff ) {
|
||||
$identifier = $this->identifier_from_xliff( $xliff );
|
||||
$job_identifier_parts = explode( '-', $identifier );
|
||||
if ( count( $job_identifier_parts ) === 2 && is_numeric( $job_identifier_parts[0] ) ) {
|
||||
$job_id = $job_identifier_parts[0];
|
||||
$job_id = apply_filters( 'wpml_job_id', $job_id );
|
||||
$md5 = $job_identifier_parts[1];
|
||||
/** @var stdClass $job */
|
||||
$job = $this->job_factory->get_translation_job( (int) $job_id, false, 1, false );
|
||||
if ( ! $job || $md5 !== md5( $job_id . $job->original_doc_id ) ) {
|
||||
$job = $this->does_not_belong_error();
|
||||
}
|
||||
} else {
|
||||
$job = $this->invalid_xliff_error();
|
||||
}
|
||||
|
||||
return $job;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $xliff_node
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_xliff_node_target( $xliff_node ) {
|
||||
$target = '';
|
||||
if ( isset( $xliff_node->target->mrk ) ) {
|
||||
$target = (string) $xliff_node->target->mrk;
|
||||
} elseif ( isset( $xliff_node->target ) ) {
|
||||
$target = (string) $xliff_node->target;
|
||||
}
|
||||
|
||||
return $target;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $validator WPML_TM_Validate_HTML
|
||||
*/
|
||||
public function set_validator( $validator ) {
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WPML_TM_Validate_HTML
|
||||
*/
|
||||
private function get_validator() {
|
||||
if ( null === $this->validator ) {
|
||||
$this->set_validator( new WPML_TM_Validate_HTML() );
|
||||
}
|
||||
|
||||
return $this->validator;
|
||||
}
|
||||
|
||||
protected function generate_job_data( SimpleXMLElement $xliff, $job ) {
|
||||
$data = [
|
||||
'job_id' => $job->job_id,
|
||||
'rid' => $job->rid,
|
||||
'fields' => [],
|
||||
'complete' => 1,
|
||||
];
|
||||
|
||||
foreach ( $xliff->file->body->children() as $node ) {
|
||||
$attr = $node->attributes();
|
||||
$type = (string) $attr['id'];
|
||||
$target = $this->get_xliff_node_target( $node );
|
||||
if ( 'html' === (string) $attr['datatype'] ) {
|
||||
$target = $this->get_validator()->restore_html( $target );
|
||||
}
|
||||
|
||||
if ( ! $this->is_valid_target( $target ) ) {
|
||||
return $this->invalid_xliff_error( array( 'target' ) );
|
||||
}
|
||||
|
||||
foreach ( $job->elements as $element ) {
|
||||
if ( $element->field_type === $type ) {
|
||||
$target = $this->replace_xliff_new_line_tag_with_new_line( $target );
|
||||
$field = array();
|
||||
$field['data'] = $target;
|
||||
$field['finished'] = 1;
|
||||
$field['tid'] = $element->tid;
|
||||
$field['field_type'] = $element->field_type;
|
||||
$field['format'] = $element->field_format;
|
||||
|
||||
$data['fields'][ $element->field_type ] = $field;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate XLIFF target on reading XLIFF.
|
||||
*
|
||||
* @param $target string
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_valid_target( $target ) {
|
||||
return $target || '0' === $target;
|
||||
}
|
||||
|
||||
protected function validate_file( $name, $content, $current_user ) {
|
||||
$xml = $this->check_xml_file( $name, $content );
|
||||
|
||||
$this->error = null;
|
||||
if ( is_wp_error( $xml ) ) {
|
||||
$this->error = $xml;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$job = $this->get_job_for_xliff( $xml );
|
||||
if ( is_wp_error( $job ) ) {
|
||||
$this->error = $job;
|
||||
|
||||
return null;
|
||||
}
|
||||
if ( ! $this->is_user_the_job_owner( $current_user, $job ) ) {
|
||||
$this->error = $this->not_the_job_owner_error( $job );
|
||||
|
||||
return null;
|
||||
}
|
||||
$job_data = $this->generate_job_data( $xml, $job );
|
||||
if ( is_wp_error( $job_data ) ) {
|
||||
$this->error = $job_data;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return array( $job, $job_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @return bool
|
||||
*/
|
||||
function validate_file_name( $filename ) {
|
||||
$ignored_files = apply_filters( 'wpml_xliff_ignored_files', array( '__MACOSX' ) );
|
||||
return ! ( preg_match( '/(\/)/', $filename ) || in_array( $filename, $ignored_files, false ) );
|
||||
}
|
||||
|
||||
protected function is_user_the_job_owner( $current_user, $job ) {
|
||||
return (int) $current_user->ID === (int) $job->translator_id;
|
||||
}
|
||||
|
||||
protected function not_the_job_owner_error( $job ) {
|
||||
$message = sprintf( __( 'The translation job (%s) doesn\'t belong to you.', 'wpml-translation-management' ), $job->job_id );
|
||||
|
||||
return new WP_Error( 'not_your_job', $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param string $content
|
||||
*
|
||||
* @return bool|SimpleXMLElement|WP_Error
|
||||
*/
|
||||
protected function check_xml_file( $name, $content ) {
|
||||
set_error_handler( array( $this, 'error_handler' ) );
|
||||
try {
|
||||
$xml = simplexml_load_string( $content );
|
||||
} catch ( Exception $e ) {
|
||||
$xml = false;
|
||||
}
|
||||
restore_error_handler();
|
||||
if ( ! $xml || ! isset( $xml->file ) ) {
|
||||
$xml = $this->not_xml_file_error( $name );
|
||||
}
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $errno
|
||||
* @param $errstr
|
||||
* @param $errfile
|
||||
* @param $errline
|
||||
*
|
||||
* @throws ErrorException
|
||||
*/
|
||||
protected function error_handler( $errno, $errstr, $errfile, $errline ) {
|
||||
throw new ErrorException( $errstr, $errno, 1, $errfile, $errline );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return WP_Error
|
||||
*/
|
||||
protected function not_xml_file_error( $name ) {
|
||||
$message = sprintf( __( '"%s" is not a valid XLIFF file.', 'wpml-translation-management' ), $name );
|
||||
|
||||
return new WP_Error( 'not_xml_file', $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $missing_data
|
||||
*
|
||||
* @return WP_Error
|
||||
*/
|
||||
protected function invalid_xliff_error( array $missing_data = array() ) {
|
||||
$message = __( 'The uploaded xliff file does not seem to be properly formed.', 'wpml-translation-management' );
|
||||
|
||||
if ( $missing_data ) {
|
||||
$message .= '<br>' . __( 'Missing or wrong data:', 'wpml-translation-management' );
|
||||
if ( count( $missing_data ) > 1 ) {
|
||||
$message .= '<ol>';
|
||||
$message .= '<li><strong>' . implode( '</strong></li><li><strong>', $missing_data ) . '</strong></li>';
|
||||
$message .= '</ol>';
|
||||
} else {
|
||||
$message .= ' <strong>' . $missing_data[0] . '</strong>';
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_Error( 'xliff_invalid', $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WP_Error
|
||||
*/
|
||||
protected function does_not_belong_error() {
|
||||
|
||||
return new WP_Error( 'xliff_does_not_match', __( "The uploaded xliff file doesn't belong to this system.", 'wpml-translation-management' ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* @author OnTheGo Systems
|
||||
*/
|
||||
class WPML_TM_XLIFF_Shortcodes extends WPML_TM_XLIFF_Phase {
|
||||
const SHORTCODE_STORE_OPTION_KEY = 'wpml_xliff_shortcodes';
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function get_data() {
|
||||
return implode( ',', $this->get_shortcodes() );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function get_shortcodes() {
|
||||
global $shortcode_tags;
|
||||
|
||||
$registered_shortcodes = array();
|
||||
|
||||
if ( $shortcode_tags ) {
|
||||
$registered_shortcodes = array_keys( $shortcode_tags );
|
||||
}
|
||||
|
||||
$stored_shortcodes = get_option( self::SHORTCODE_STORE_OPTION_KEY, array() );
|
||||
|
||||
return $this->get_sanitized_shortcodes( $registered_shortcodes, $stored_shortcodes );
|
||||
}
|
||||
|
||||
private function get_sanitized_shortcodes( array $shortcodes1, array $shortcodes2 ) {
|
||||
return array_unique( array_filter( apply_filters( 'wpml_shortcode_list', array_merge( $shortcodes1, $shortcodes2 ) ) ) );
|
||||
}
|
||||
|
||||
protected function get_phase_name() {
|
||||
return 'shortcodes';
|
||||
}
|
||||
|
||||
protected function get_process_name() {
|
||||
return 'Shortcodes identification';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
/**
|
||||
* @author OnTheGo Systems
|
||||
*/
|
||||
class WPML_TM_XLIFF_Translator_Notes extends WPML_TM_XLIFF_Phase {
|
||||
|
||||
private $post_id;
|
||||
|
||||
public function __construct( $post_id = 0 ) {
|
||||
$this->post_id = $post_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function get_data() {
|
||||
if ( $this->post_id ) {
|
||||
return WPML_TM_Translator_Note::get( $this->post_id );
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
protected function get_phase_name() {
|
||||
return 'notes';
|
||||
}
|
||||
|
||||
protected function get_process_name() {
|
||||
return 'Notes';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
<?php
|
||||
/**
|
||||
* @package wpml-core
|
||||
*/
|
||||
|
||||
use WPML\FP\Obj;
|
||||
use WPML\TM\Jobs\FieldId;
|
||||
|
||||
class WPML_TM_Xliff_Writer {
|
||||
const TAB = "\t";
|
||||
|
||||
protected $job_factory;
|
||||
private $xliff_version;
|
||||
private $xliff_shortcodes;
|
||||
private $translator_notes;
|
||||
|
||||
/**
|
||||
* WPML_TM_xliff constructor.
|
||||
*
|
||||
* @param WPML_Translation_Job_Factory $job_factory
|
||||
* @param string $xliff_version
|
||||
* @param \WPML_TM_XLIFF_Shortcodes|null $xliff_shortcodes
|
||||
*/
|
||||
public function __construct( WPML_Translation_Job_Factory $job_factory, $xliff_version = TRANSLATION_PROXY_XLIFF_VERSION, WPML_TM_XLIFF_Shortcodes $xliff_shortcodes = null ) {
|
||||
$this->job_factory = $job_factory;
|
||||
$this->xliff_version = $xliff_version;
|
||||
|
||||
if ( ! $xliff_shortcodes ) {
|
||||
$xliff_shortcodes = wpml_tm_xliff_shortcodes();
|
||||
}
|
||||
$this->xliff_shortcodes = $xliff_shortcodes;
|
||||
$this->translator_notes = new WPML_TM_XLIFF_Translator_Notes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a XLIFF file for a given job.
|
||||
*
|
||||
* @param int $job_id
|
||||
*
|
||||
* @return resource XLIFF representation of the job
|
||||
*/
|
||||
public function get_job_xliff_file( $job_id ) {
|
||||
|
||||
return $this->generate_xliff_file( $this->generate_job_xliff( $job_id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a XLIFF string for a given post or external type (e.g. package) job.
|
||||
*
|
||||
* @param int $job_id
|
||||
*
|
||||
* @return string XLIFF representation of the job
|
||||
*/
|
||||
public function generate_job_xliff( $job_id ) {
|
||||
/** @var TranslationManagement $iclTranslationManagement */
|
||||
global $iclTranslationManagement;
|
||||
|
||||
// don't include not-translatable and don't auto-assign
|
||||
$job = $iclTranslationManagement->get_translation_job( (int) $job_id, false, false, 1 );
|
||||
$translation_units = $this->get_job_translation_units_data( $job );
|
||||
$original = $job_id . '-' . md5( $job_id . $job->original_doc_id );
|
||||
$original_post_type = isset( $job->original_post_type ) ? $job->original_post_type : null;
|
||||
|
||||
$external_file_url = $this->get_external_url( $job );
|
||||
$this->get_translator_notes( $job );
|
||||
|
||||
$xliff = $this->generate_xliff(
|
||||
$original,
|
||||
$job->source_language_code,
|
||||
$job->language_code,
|
||||
$translation_units,
|
||||
$external_file_url,
|
||||
$original_post_type
|
||||
);
|
||||
|
||||
return $xliff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a XLIFF file for a given set of strings.
|
||||
*
|
||||
* @param array $strings
|
||||
* @param string $source_language
|
||||
* @param string $target_language
|
||||
*
|
||||
* @return resource XLIFF file
|
||||
*/
|
||||
public function get_strings_xliff_file( $strings, $source_language, $target_language ) {
|
||||
$strings = $this->pre_populate_strings_with_translation_memory( $strings, $source_language, $target_language );
|
||||
|
||||
return $this->generate_xliff_file(
|
||||
$this->generate_xliff(
|
||||
uniqid( 'string-', true ),
|
||||
$source_language,
|
||||
$target_language,
|
||||
$this->generate_strings_translation_units_data( $strings )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private function generate_xliff(
|
||||
$original_id,
|
||||
$source_language,
|
||||
$target_language,
|
||||
array $translation_units = array(),
|
||||
$external_file_url = null,
|
||||
$original_post_type = null
|
||||
) {
|
||||
$xliff = new WPML_TM_XLIFF( $this->get_xliff_version(), '1.0', 'utf-8' );
|
||||
|
||||
$phase_group = array();
|
||||
$phase_group = array_merge( $phase_group, $this->xliff_shortcodes->get() );
|
||||
$phase_group = array_merge( $phase_group, $this->translator_notes->get() );
|
||||
$post_type_phase = new WPML_TM_XLIFF_Post_Type( $original_post_type );
|
||||
$phase_group = array_merge( $phase_group, $post_type_phase->get() );
|
||||
|
||||
$string = $xliff
|
||||
->setFileAttributes(
|
||||
array(
|
||||
'original' => $original_id,
|
||||
'source-language' => $source_language,
|
||||
'target-language' => $target_language,
|
||||
'datatype' => 'plaintext',
|
||||
)
|
||||
)
|
||||
->setReferences(
|
||||
array(
|
||||
'external-file' => $external_file_url,
|
||||
)
|
||||
)
|
||||
->setPhaseGroup( $phase_group )
|
||||
->setTranslationUnits( $translation_units )
|
||||
->toString();
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
private function get_xliff_version() {
|
||||
switch ( $this->xliff_version ) {
|
||||
case '10':
|
||||
return '1.0';
|
||||
case '11':
|
||||
return '1.1';
|
||||
case '12':
|
||||
default:
|
||||
return '1.2';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate translation units for a given set of strings.
|
||||
*
|
||||
* The units are the actual content to be translated
|
||||
* Represented as a source and a target
|
||||
*
|
||||
* @param array $strings
|
||||
*
|
||||
* @return array The translation units representation
|
||||
*/
|
||||
private function generate_strings_translation_units_data( $strings ) {
|
||||
$translation_units = array();
|
||||
|
||||
foreach ( $strings as $string ) {
|
||||
$id = 'string-' . $string->id;
|
||||
$translation_units[] = $this->get_translation_unit_data( $id, 'string', $string->value, $string->translation, $string->translated_from_memory );
|
||||
}
|
||||
|
||||
return $translation_units;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass[] $strings
|
||||
* @param string $source_lang
|
||||
* @param string $target_lang
|
||||
*
|
||||
* @return stdClass[]
|
||||
*/
|
||||
private function pre_populate_strings_with_translation_memory( $strings, $source_lang, $target_lang ) {
|
||||
$strings_to_translate = wp_list_pluck( $strings, 'value' );
|
||||
$original_translated_map = $this->get_original_translated_map_from_translation_memory( $strings_to_translate, $source_lang, $target_lang );
|
||||
|
||||
foreach ( $strings as &$string ) {
|
||||
$string->translated_from_memory = false;
|
||||
$string->translation = $string->value;
|
||||
|
||||
if ( array_key_exists( $string->value, $original_translated_map ) ) {
|
||||
$string->translation = $original_translated_map[ $string->value ];
|
||||
$string->translated_from_memory = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $strings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $strings_to_translate
|
||||
* @param string $source_lang
|
||||
* @param string $target_lang
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_original_translated_map_from_translation_memory( $strings_to_translate, $source_lang, $target_lang ) {
|
||||
$args = array(
|
||||
'strings' => $strings_to_translate,
|
||||
'source_lang' => $source_lang,
|
||||
'target_lang' => $target_lang,
|
||||
);
|
||||
|
||||
$strings_in_memory = apply_filters( 'wpml_st_get_translation_memory', array(), $args );
|
||||
|
||||
if ( $strings_in_memory ) {
|
||||
return wp_list_pluck( $strings_in_memory, 'translation', 'original' );
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate translation units.
|
||||
*
|
||||
* The units are the actual content to be translated
|
||||
* Represented as a source and a target
|
||||
*
|
||||
* @param stdClass $job
|
||||
*
|
||||
* @return array The translation units data
|
||||
*/
|
||||
private function get_job_translation_units_data( $job ) {
|
||||
$translation_units = array();
|
||||
/** @var array $elements */
|
||||
$elements = $job->elements;
|
||||
if ( $elements ) {
|
||||
$elements = $this->pre_populate_elements_with_translation_memory( $elements, $job->source_language_code, $job->language_code );
|
||||
|
||||
foreach ( $elements as $element ) {
|
||||
if ( 1 === (int) $element->field_translate ) {
|
||||
$field_data_translated = base64_decode( $element->field_data_translated );
|
||||
$field_data = base64_decode( $element->field_data );
|
||||
|
||||
/**
|
||||
* It modifies the content of a single field data which represents, for example, one paragraph in post content.
|
||||
*
|
||||
* @since 2.10.0
|
||||
* @param string $field_data
|
||||
*/
|
||||
$field_data = apply_filters( 'wpml_tm_xliff_unit_field_data', $field_data );
|
||||
|
||||
if ( 0 === strpos( $element->field_type, 'field-' ) ) {
|
||||
$field_data_translated = apply_filters(
|
||||
'wpml_tm_xliff_export_translated_cf',
|
||||
$field_data_translated,
|
||||
$element
|
||||
);
|
||||
$field_data = apply_filters(
|
||||
'wpml_tm_xliff_export_original_cf',
|
||||
$field_data,
|
||||
$element
|
||||
);
|
||||
}
|
||||
// check for untranslated fields and copy the original if required.
|
||||
if ( ! null === $field_data_translated || '' === $field_data_translated ) {
|
||||
$field_data_translated = $this->remove_invalid_chars( $field_data );
|
||||
}
|
||||
if ( $this->is_valid_unit_content( $field_data ) ) {
|
||||
$translation_units[] = $this->get_translation_unit_data(
|
||||
$element->field_type,
|
||||
$element->field_type,
|
||||
$field_data,
|
||||
$field_data_translated,
|
||||
$element->translated_from_memory,
|
||||
$element->field_wrap_tag,
|
||||
$this->get_field_title( $element, $job )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $translation_units;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \stdClass $field
|
||||
* @param \stdClass $job
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_field_title( $field, $job ) {
|
||||
$result = apply_filters( 'wpml_tm_adjust_translation_fields', [ (array) $field ], $job, null );
|
||||
|
||||
return Obj::pathOr( '', [ 0, 'title' ], $result );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $elements
|
||||
* @param string $source_lang
|
||||
* @param string $target_lang
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function pre_populate_elements_with_translation_memory( array $elements, $source_lang, $target_lang ) {
|
||||
$strings_to_translate = array();
|
||||
|
||||
foreach ( $elements as &$element ) {
|
||||
|
||||
if ( preg_match( '/^package-string/', $element->field_type ) ) {
|
||||
$strings_to_translate[ $element->tid ] = base64_decode( $element->field_data );
|
||||
}
|
||||
|
||||
$element->translated_from_memory = FieldId::is_any_term_field( $element->field_type )
|
||||
&& $element->field_data_translated
|
||||
&& $element->field_data != $element->field_data_translated;
|
||||
}
|
||||
|
||||
$original_translated_map = $this->get_original_translated_map_from_translation_memory( $strings_to_translate, $source_lang, $target_lang );
|
||||
|
||||
if ( $original_translated_map ) {
|
||||
|
||||
foreach ( $elements as &$element ) {
|
||||
|
||||
if ( array_key_exists( $element->tid, $strings_to_translate )
|
||||
&& array_key_exists( $strings_to_translate[ $element->tid ], $original_translated_map )
|
||||
) {
|
||||
$element->field_data_translated = base64_encode( $original_translated_map[ $strings_to_translate[ $element->tid ] ] );
|
||||
$element->translated_from_memory = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation unit data.
|
||||
*
|
||||
* @param string $field_id Field ID.
|
||||
* @param string $field_name Field name.
|
||||
* @param string $field_data Field content.
|
||||
* @param string $field_data_translated Field translated content.
|
||||
* @param boolean $is_translated_from_memory Boolean flag - is translated from memory.
|
||||
* @param string $field_wrap_tag Field wrap tag (h1...h6, etc.)
|
||||
* @param string $title
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_translation_unit_data(
|
||||
$field_id,
|
||||
$field_name,
|
||||
$field_data,
|
||||
$field_data_translated,
|
||||
$is_translated_from_memory = false,
|
||||
$field_wrap_tag = '',
|
||||
$title = ''
|
||||
) {
|
||||
global $sitepress;
|
||||
|
||||
$field_data = $this->remove_invalid_chars( $field_data );
|
||||
|
||||
$translation_unit = array();
|
||||
|
||||
$field_data = $this->remove_line_breaks_inside_tags( $field_data );
|
||||
$field_data_translated = $this->remove_line_breaks_inside_tags( $field_data_translated );
|
||||
|
||||
if ( $sitepress->get_setting( 'xliff_newlines' ) === WPML_XLIFF_TM_NEWLINES_REPLACE ) {
|
||||
$field_data = $this->replace_new_line_with_tag( $field_data );
|
||||
$field_data_translated = $this->replace_new_line_with_tag( $field_data_translated );
|
||||
}
|
||||
|
||||
if ( $title ) {
|
||||
$translation_unit['attributes']['extradata'] = $title;
|
||||
}
|
||||
$translation_unit['attributes']['resname'] = $field_name;
|
||||
$translation_unit['attributes']['restype'] = 'string';
|
||||
$translation_unit['attributes']['datatype'] = 'html';
|
||||
$translation_unit['attributes']['id'] = $field_id;
|
||||
$translation_unit['source'] = array( 'content' => $field_data );
|
||||
$translation_unit['target'] = array( 'content' => $field_data_translated );
|
||||
$translation_unit['note'] = array( 'content' => $field_wrap_tag );
|
||||
|
||||
if ( $is_translated_from_memory ) {
|
||||
$translation_unit['target']['attributes'] = array(
|
||||
'state' => 'needs-review-translation',
|
||||
'state-qualifier' => 'tm-suggestion',
|
||||
);
|
||||
}
|
||||
|
||||
return $translation_unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function replace_new_line_with_tag( $string ) {
|
||||
return str_replace( array( "\n", "\r" ), array( '<br class="xliff-newline" />', '' ), $string );
|
||||
}
|
||||
|
||||
private function remove_line_breaks_inside_tags( $string ) {
|
||||
return preg_replace_callback( '/(<[^>]*>)/m', array( $this, 'remove_line_breaks_inside_tag_callback' ), $string );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $matches
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function remove_line_breaks_inside_tag_callback( array $matches ) {
|
||||
$tag_string = preg_replace( '/([\n\r\t ]+)/', ' ', $matches[0] );
|
||||
$tag_string = preg_replace( '/(<[\s]+)/', '<', $tag_string );
|
||||
return preg_replace( '/([\s]+>)/', '>', $tag_string );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
*
|
||||
* Remove all characters below 0x20 except for 0x09, 0x0A and 0x0D
|
||||
* @see https://www.w3.org/TR/xml/#charsets
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
|
||||
private function remove_invalid_chars( $string ) {
|
||||
return preg_replace( '/[\x00-\x08\x0B-\x0C\x0E-\x1F]/', '', $string );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a xliff string to a temporary file and return the file ressource
|
||||
* handle
|
||||
*
|
||||
* @param string $xliff_content
|
||||
*
|
||||
* @return resource XLIFF
|
||||
*/
|
||||
private function generate_xliff_file( $xliff_content ) {
|
||||
$file = fopen( 'php://temp', 'rb+' );
|
||||
fwrite( $file, $xliff_content );
|
||||
rewind( $file );
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $job
|
||||
*
|
||||
* @return false|null|string
|
||||
*/
|
||||
private function get_external_url( $job ) {
|
||||
$external_file_url = null;
|
||||
if ( isset( $job->original_doc_id ) && 'post' === $job->element_type_prefix ) {
|
||||
$external_file_url = get_permalink( $job->original_doc_id );
|
||||
|
||||
return $external_file_url;
|
||||
}
|
||||
|
||||
return $external_file_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $content
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_valid_unit_content( $content ) {
|
||||
$content = preg_replace( '/[^#\w]*/u', '', $content );
|
||||
|
||||
return $content || '0' === $content;
|
||||
}
|
||||
|
||||
private function get_translator_notes( $job ) {
|
||||
$this->translator_notes = new WPML_TM_XLIFF_Translator_Notes(
|
||||
isset( $job->original_doc_id ) ? $job->original_doc_id : 0
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
abstract class WPML_TM_XLIFF_Phase {
|
||||
|
||||
public function get() {
|
||||
$data = $this->get_data();
|
||||
|
||||
if ( $data ) {
|
||||
return array(
|
||||
$this->get_phase_name() => array(
|
||||
'process-name' => $this->get_process_name(),
|
||||
'note' => $data,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
abstract protected function get_data();
|
||||
abstract protected function get_phase_name();
|
||||
abstract protected function get_process_name();
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @author OnTheGo Systems
|
||||
*/
|
||||
class WPML_TM_XLIFF {
|
||||
/** @var DOMElement */
|
||||
private $body;
|
||||
/** @var DOMDocument */
|
||||
private $dom;
|
||||
/** @var DOMDocumentType */
|
||||
private $dtd;
|
||||
/** @var DOMElement */
|
||||
private $file;
|
||||
/** @var DOMElement */
|
||||
private $file_header;
|
||||
/** @var DOMElement */
|
||||
private $file_reference;
|
||||
/** @var DOMElement */
|
||||
private $phase_group;
|
||||
/** @var DOMElement */
|
||||
private $root;
|
||||
/** @var array */
|
||||
private $trans_units;
|
||||
/** @var string */
|
||||
private $xliff_version;
|
||||
|
||||
/**
|
||||
* WPML_TM_XLIFF constructor.
|
||||
*
|
||||
* @param string $xliff_version
|
||||
* @param string $xml_version
|
||||
* @param string $xml_encoding
|
||||
*/
|
||||
public function __construct( $xliff_version = '1.2', $xml_version = '1.0', $xml_encoding = 'utf-8' ) {
|
||||
$this->dom = new DOMDocument( $xml_version, $xml_encoding );
|
||||
|
||||
$this->root = $this->dom->createElement( 'xliff' );
|
||||
$this->file = $this->dom->createElement( 'file' );
|
||||
$this->file_header = $this->dom->createElement( 'header' );
|
||||
$this->body = $this->dom->createElement( 'body' );
|
||||
|
||||
$this->xliff_version = $xliff_version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $attributes
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setFileAttributes( $attributes ) {
|
||||
foreach ( $attributes as $name => $value ) {
|
||||
$this->file->setAttribute( $name, $value );
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $args
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setPhaseGroup( array $args ) {
|
||||
if ( $args ) {
|
||||
$phase_items = array();
|
||||
foreach ( $args as $name => $data ) {
|
||||
if (
|
||||
$name
|
||||
&& array_key_exists( 'process-name', $data )
|
||||
&& array_key_exists( 'note', $data )
|
||||
&& $data['note']
|
||||
) {
|
||||
|
||||
$phase = $this->dom->createElement( 'phase' );
|
||||
$phase->setAttribute( 'phase-name', $name );
|
||||
$phase->setAttribute( 'process-name', $data['process-name'] );
|
||||
$note = $this->dom->createElement( 'note' );
|
||||
$note->nodeValue = $data['note'];
|
||||
$phase->appendChild( $note );
|
||||
$phase_items[] = $phase;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if ( $phase_items ) {
|
||||
$this->phase_group = $this->dom->createElement( 'phase-group' );
|
||||
foreach ( $phase_items as $phase_item ) {
|
||||
$this->phase_group->appendChild( $phase_item );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $references
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setReferences( array $references ) {
|
||||
if ( $references ) {
|
||||
foreach ( $references as $name => $value ) {
|
||||
if ( ! $value ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! $this->file_reference ) {
|
||||
$this->file_reference = $this->dom->createElement( 'reference' );
|
||||
}
|
||||
|
||||
$reference = $this->dom->createElement( $name );
|
||||
$reference->setAttribute( 'href', $value );
|
||||
$this->file_reference->appendChild( $reference );
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
|
||||
|
||||
/**
|
||||
* Set translation units for xliff.
|
||||
*
|
||||
* @param array $trans_units Translation units.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setTranslationUnits( $trans_units ) {
|
||||
// phpcs:enable
|
||||
if ( $trans_units ) {
|
||||
foreach ( $trans_units as $trans_unit ) {
|
||||
$trans_unit_element = $this->dom->createElement( 'trans-unit' );
|
||||
if ( array_key_exists( 'attributes', $trans_unit ) && is_array( $trans_unit['attributes'] ) ) {
|
||||
foreach ( $trans_unit['attributes'] as $name => $value ) {
|
||||
$trans_unit_element->setAttribute( $name, $value );
|
||||
}
|
||||
}
|
||||
$this->appendData( 'source', $trans_unit, $trans_unit_element );
|
||||
$this->appendData( 'target', $trans_unit, $trans_unit_element );
|
||||
|
||||
if ( $trans_unit['note']['content'] ) {
|
||||
$note = $this->dom->createElement( 'note' );
|
||||
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||
$note->nodeValue = 'wrap_tag:' . $trans_unit['note']['content'];
|
||||
// phpcs:enable
|
||||
$trans_unit_element->appendChild( $note );
|
||||
}
|
||||
|
||||
$this->trans_units[] = $trans_unit_element;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param array $trans_unit
|
||||
* @param DOMElement $trans_unit_element
|
||||
*/
|
||||
private function appendData( $type, $trans_unit, $trans_unit_element ) {
|
||||
if ( array_key_exists( $type, $trans_unit ) ) {
|
||||
$source = $this->dom->createElement( $type );
|
||||
$datatype = isset( $trans_unit['attributes']['datatype'] ) ? $trans_unit['attributes']['datatype'] : '';
|
||||
$source_cdata = $this->dom->createCDATASection(
|
||||
$this->validate( $datatype, $trans_unit[ $type ]['content'] )
|
||||
);
|
||||
$source->appendChild( $source_cdata );
|
||||
|
||||
if ( array_key_exists( 'attributes', $trans_unit[ $type ] ) ) {
|
||||
|
||||
foreach ( $trans_unit[ $type ]['attributes'] as $name => $value ) {
|
||||
$source->setAttribute( $name, $value );
|
||||
}
|
||||
}
|
||||
|
||||
$trans_unit_element->appendChild( $source );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate content.
|
||||
*
|
||||
* @param string $datatype Type of content data.
|
||||
* @param string $content Content.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function validate( $datatype, $content ) {
|
||||
if ( 'html' === $datatype ) {
|
||||
$validator = new WPML_TM_Validate_HTML();
|
||||
|
||||
$validator->validate( $content );
|
||||
return $validator->get_html();
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
public function toString() {
|
||||
$this->compose();
|
||||
|
||||
return trim( $this->dom->saveXML() );
|
||||
}
|
||||
|
||||
private function compose() {
|
||||
$this->dom->xmlStandalone = false;
|
||||
|
||||
$this->setRoot( $this->xliff_version );
|
||||
|
||||
if ( $this->dtd ) {
|
||||
$this->dom->appendChild( $this->dtd );
|
||||
}
|
||||
|
||||
if ( $this->phase_group ) {
|
||||
$this->file_header->appendChild( $this->phase_group );
|
||||
}
|
||||
if ( $this->file_reference ) {
|
||||
$this->file_header->appendChild( $this->file_reference );
|
||||
}
|
||||
|
||||
if ( $this->trans_units ) {
|
||||
foreach ( $this->trans_units as $trans_unit ) {
|
||||
$this->body->appendChild( $trans_unit );
|
||||
}
|
||||
}
|
||||
|
||||
$this->file->appendChild( $this->file_header );
|
||||
$this->file->appendChild( $this->body );
|
||||
$this->root->appendChild( $this->file );
|
||||
$this->dom->appendChild( $this->root );
|
||||
}
|
||||
|
||||
private function setRoot( $version ) {
|
||||
if ( $version === '1.0' ) {
|
||||
$implementation = new DOMImplementation();
|
||||
|
||||
$this->dtd = $implementation->createDocumentType(
|
||||
'xliff',
|
||||
'-//XLIFF//DTD XLIFF//EN',
|
||||
'http://www.oasis-open.org/committees/xliff/documents/xliff.dtd'
|
||||
);
|
||||
}
|
||||
$this->root->setAttribute( 'version', $version );
|
||||
$this->root->setAttribute( 'xmlns', 'urn:oasis:names:tc:xliff:document:' . $version );
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user