first commit

This commit is contained in:
2023-09-12 21:41:04 +02:00
commit 3361a7f053
13284 changed files with 2116755 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
<?php
namespace WPML\ST\Rest;
abstract class Base extends \WPML\Rest\Base {
/**
* @return string
*/
public function get_namespace() {
return 'wpml/st/v1';
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace WPML\ST\Rest;
use WPML\ST\MO\File\ManagerFactory;
use WPML\ST\MO\File\Manager;
use function WPML\Container\make;
/**
* @author OnTheGo Systems
*/
class FactoryLoader implements \IWPML_REST_Action_Loader, \IWPML_Deferred_Action_Loader {
const REST_API_INIT_ACTION = 'rest_api_init';
/**
* @return string
*/
public function get_load_action() {
return self::REST_API_INIT_ACTION;
}
public function create() {
return [
MO\Import::class => make( MO\Import::class ),
MO\PreGenerate::class => $this->create_pre_generate(),
Settings::class => make( Settings::class ),
];
}
private function create_pre_generate() {
/** @var Manager $manager */
$manager = ManagerFactory::create();
return make(
MO\PreGenerate::class,
[ ':manager' => $manager ]
);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace WPML\ST\Rest\MO;
use WPML\ST\TranslationFile\QueueFilter;
use WPML_ST_Translations_File_Queue;
class Import extends \WPML\ST\Rest\Base {
/**
* @return array
*/
function get_routes() {
return [
[
'route' => 'import_mo_strings',
'args' => [
'methods' => 'POST',
'callback' => [ $this, 'import' ],
'args' => [
'plugins' => [
'type' => 'array',
],
'themes' => [
'type' => 'array',
],
'other' => [
'type' => 'array',
],
]
]
]
];
}
/**
* @param \WP_REST_Request $request
*
* @return array
*/
function get_allowed_capabilities( \WP_REST_Request $request ) {
return [ 'manage_options' ];
}
/**
* @return array
* @throws \WPML\Auryn\InjectionException
*/
public function import( \WP_REST_Request $request ) {
/** @var WPML_ST_Translations_File_Queue $queue */
$queue = \WPML\Container\make( \WPML_ST_Translations_File_Scan_Factory::class )->create_queue();
$queue->import( new QueueFilter(
$request->get_param( 'plugins' ),
$request->get_param( 'themes' ),
$request->get_param( 'other' )
) );
return [ 'remaining' => $queue->get_pending() ];
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace WPML\ST\Rest\MO;
use WPML\ST\MO\File\Manager;
use \WPML\Rest\Adaptor;
use WPML\ST\MO\Generate\Process\ProcessFactory;
use WPML\ST\MO\Scan\UI\Factory;
class PreGenerate extends \WPML\ST\Rest\Base {
/** @var Manager */
private $manager;
/** @var ProcessFactory */
private $processFactory;
public function __construct(
Adaptor $adaptor,
Manager $manager,
ProcessFactory $processFactory
) {
parent::__construct( $adaptor );
$this->manager = $manager;
$this->processFactory = $processFactory;
}
/**
* @return array
*/
function get_routes() {
return [
[
'route' => 'pre_generate_mo_files',
'args' => [
'methods' => 'POST',
'callback' => [ $this, 'generate' ],
'args' => [],
]
]
];
}
function get_allowed_capabilities( \WP_REST_Request $request ) {
return [ 'manage_options' ];
}
public function generate() {
if ( ! $this->manager->maybeCreateSubdir() ) {
return [ 'error' => 'no access' ];
}
Factory::clearIgnoreWpmlVersion();
return [ 'remaining' => $this->processFactory->create()->runPage() ];
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace WPML\ST\Rest;
class Settings extends Base {
/** @var \WPML\WP\OptionManager $option_manager */
private $option_manager;
public function __construct( \WPML\Rest\Adaptor $adaptor, \WPML\WP\OptionManager $option_manager ) {
parent::__construct( $adaptor );
$this->option_manager = $option_manager;
}
public function get_routes() {
return [
[
'route' => 'settings',
'args' => [
'methods' => 'POST',
'callback' => [ $this, 'set' ],
'args' => [
'group' => [ 'required' => true ],
'key' => [ 'required' => true ],
'data' => [ 'required' => true ],
],
],
],
];
}
public function get_allowed_capabilities( \WP_REST_Request $request ) {
return [ 'manage_options' ];
}
public function set( \WP_REST_Request $request ) {
$group = $request->get_param( 'group' );
$key = $request->get_param( 'key' );
$data = $request->get_param( 'data' );
$this->option_manager->set( 'ST-' . $group, $key, $data );
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace WPML\ST\MO\File;
use WPML\ST\TranslationFile\StringEntity;
class Builder extends \WPML\ST\TranslationFile\Builder {
/** @var Generator */
private $generator;
public function __construct( Generator $generator ) {
$this->generator = $generator;
}
/**
* @param StringEntity[] $strings
* @return string
*/
public function get_content( array $strings ) {
return $this->generator->getContent( $strings );
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace WPML\ST\MO\File;
use WP_Filesystem_Direct;
use WPML\ST\MO\Generate\Process\Status;
use WPML\ST\MO\Generate\Process\SingleSiteProcess;
use WPML\ST\MO\Notice\RegenerationInProgressNotice;
use function wpml_get_admin_notices;
class FailureHooks implements \IWPML_Backend_Action {
use makeDir;
const NOTICE_GROUP = 'mo-failure';
const NOTICE_ID_MISSING_FOLDER = 'missing-folder';
/** @var Status */
private $status;
/** @var SingleSiteProcess $singleProcess */
private $singleProcess;
public function __construct(
WP_Filesystem_Direct $filesystem,
Status $status,
SingleSiteProcess $singleProcess
) {
$this->filesystem = $filesystem;
$this->status = $status;
$this->singleProcess = $singleProcess;
}
public function add_hooks() {
add_action( 'admin_init', [ $this, 'checkDirectories' ] );
}
public function checkDirectories() {
if ( $this->isDirectoryMissing( WP_LANG_DIR ) ) {
$this->resetRegenerateStatus();
$this->displayMissingFolderNotice( WP_LANG_DIR );
return;
}
if ( $this->isDirectoryMissing( self::getSubdir() ) ) {
$this->resetRegenerateStatus();
if ( ! $this->maybeCreateSubdir() ) {
$this->displayMissingFolderNotice( self::getSubdir() );
return;
}
}
if ( ! $this->status->isComplete() ) {
$this->displayRegenerateInProgressNotice();
$this->singleProcess->runPage();
}
if ( $this->status->isComplete() ) {
wpml_get_admin_notices()->remove_notice( RegenerationInProgressNotice::GROUP, RegenerationInProgressNotice::ID );
}
}
/**
* @param string $dir
*/
public function displayMissingFolderNotice( $dir ) {
$notices = wpml_get_admin_notices();
$notice = $notices->get_new_notice(
self::NOTICE_ID_MISSING_FOLDER, self::missingFolderNoticeContent( $dir ),
self::NOTICE_GROUP
);
$notice->set_css_classes( 'error' );
$notices->add_notice( $notice );
}
/**
* @param string $dir
*
* @return string
*/
public static function missingFolderNoticeContent( $dir ) {
$text = '<p>' .
esc_html__( 'WPML String Translation is attempting to write .mo files with translations to folder:',
'wpml-string-translation' ) . '<br/>' .
str_replace( '\\', '/', $dir ) .
'</p>';
$text .= '<p>' . esc_html__( 'This folder appears to be not writable. This is blocking translation for strings from appearing on the site.',
'wpml-string-translation' ) . '</p>';
$text .= '<p>' . esc_html__( 'To resolve this, please contact your hosting company and request that they make that folder writable.',
'wpml-string-translation' ) . '</p>';
$url = 'https://wpml.org/faq/cannot-write-mo-files/?utm_source=plugin&utm_medium=gui&utm_campaign=wpmlst';
$link = '<a href="' . $url . '" target="_blank" rel="noreferrer noopener" >' .
esc_html__( "WPML's documentation on troubleshooting .mo files generation.",
'wpml-string-translation' ) .
'</a>';
$text .= '<p>' . sprintf( esc_html__( 'For more details, see %s.', 'wpml-string-translation' ),
$link ) . '</p>';
return $text;
}
private function displayRegenerateInProgressNotice() {
$notices = wpml_get_admin_notices();
$notices->remove_notice( self::NOTICE_GROUP, self::NOTICE_ID_MISSING_FOLDER );
$notices->add_notice( new RegenerationInProgressNotice() );
}
/**
* @return string
*/
public static function getSubdir() {
return WP_LANG_DIR . '/' . \WPML\ST\TranslationFile\Manager::SUB_DIRECTORY;
}
/**
* @param string $dir
*
* @return bool
*/
private function isDirectoryMissing( $dir ) {
return ! $this->filesystem->is_writable( $dir );
}
private function resetRegenerateStatus() {
$this->status->markIncomplete();
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace WPML\ST\MO\File;
use SitePress;
use WPML\ST\MO\Generate\Process\ProcessFactory;
use function WPML\Container\make;
use WPML\ST\MO\Scan\UI\Factory as UiFactory;
class FailureHooksFactory implements \IWPML_Backend_Action_Loader {
/**
* @return FailureHooks|null
* @throws \WPML\Auryn\InjectionException
*/
public function create() {
/** @var SitePress $sitepress */
global $sitepress;
if ( $sitepress->is_setup_complete() && $this->hasRanPreGenerateViaUi() ) {
$inBackground = true;
return make( FailureHooks::class, [
':status' => ProcessFactory::createStatus( $inBackground ),
':singleProcess' => ProcessFactory::createSingle( $inBackground ),
] );
}
return null;
}
/**
* @return bool
* @throws \WPML\Auryn\InjectionException
*/
private function hasRanPreGenerateViaUi() {
$uiPreGenerateStatus = ProcessFactory::createStatus( false );
return $uiPreGenerateStatus->isComplete()
|| UiFactory::isDismissed()
|| ! ProcessFactory::createSingle()->getPagesCount();
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace WPML\ST\MO\File;
use WPML\Collect\Support\Collection;
use WPML\ST\TranslateWpmlString;
use WPML\ST\TranslationFile\StringEntity;
use function wpml_collect;
class Generator {
/** @var MOFactory */
private $moFactory;
public function __construct( MOFactory $moFactory ) {
$this->moFactory = $moFactory;
}
/**
* @param StringEntity[] $entries
*
* @return string
*/
public function getContent( array $entries ) {
$mo = $this->moFactory->createNewInstance();
wpml_collect( $entries )
->reduce( [ $this, 'createMOFormatEntities' ], wpml_collect( [] ) )
->filter( function( array $entry ) { return ! empty($entry['singular']); } )
->each( [ $mo, 'add_entry' ] );
$mem_file = fopen( 'php://memory', 'r+' );
$mo->export_to_file_handle( $mem_file );
rewind( $mem_file );
$mo_content = stream_get_contents( $mem_file );
fclose( $mem_file );
return $mo_content;
}
/**
* @param Collection $carry
* @param StringEntity $entry
*
* @return Collection
*/
public function createMOFormatEntities( $carry, StringEntity $entry ) {
$carry->push( $this->mapStringEntityToMOFormatUsing( $entry, 'original' ) );
if ( TranslateWpmlString::canTranslateWithMO( $entry->get_original(), $entry->get_name() ) ) {
$carry->push( $this->mapStringEntityToMOFormatUsing( $entry, 'name' ) );
}
return $carry;
}
/**
* @param StringEntity $entry
* @param string $singularField
*
* @return array
*/
private function mapStringEntityToMOFormatUsing( StringEntity $entry, $singularField ) {
return [
'singular' => $entry->{'get_' . $singularField}(),
'translations' => $entry->get_translations(),
'context' => $entry->get_context(),
'plural' => $entry->get_original_plural(),
];
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace WPML\ST\MO\File;
class MOFactory {
/**
* @return \MO
*/
public function createNewInstance() {
return new \MO();
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace WPML\ST\MO\File;
use GlobIterator;
use WPML\Collect\Support\Collection;
use WPML\ST\TranslationFile\Domains;
use WPML\ST\TranslationFile\StringsRetrieve;
use WPML_Language_Records;
class Manager extends \WPML\ST\TranslationFile\Manager {
public function __construct(
StringsRetrieve $strings,
Builder $builder,
\WP_Filesystem_Direct $filesystem,
WPML_Language_Records $language_records,
Domains $domains
) {
parent::__construct( $strings, $builder, $filesystem, $language_records, $domains );
}
/**
* @return string
*/
protected function getFileExtension() {
return 'mo';
}
/**
* @return bool
*/
public function isPartialFile() {
return true;
}
/**
* @return Collection
*/
protected function getDomains() {
return $this->domains->getMODomains();
}
/**
* @return bool
*/
public static function hasFiles() {
return (bool) ( new GlobIterator( self::getSubdir() . '/*.mo' ) )->count();
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace WPML\ST\MO\File;
use function WPML\Container\make;
class ManagerFactory {
/**
* @return Manager
* @throws \WPML\Auryn\InjectionException
*/
public static function create() {
return make( Manager::class, [ ':builder' => make( Builder::class ) ] );
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace WPML\ST\MO\File;
trait makeDir {
/**
* @var \WP_Filesystem_Direct
*/
protected $filesystem;
/** @return bool */
public function maybeCreateSubdir() {
$subdir = $this->getSubdir();
if ( $this->filesystem->is_dir( $subdir ) && $this->filesystem->is_writable( $subdir ) ) {
return true;
}
return $this->filesystem->mkdir( $subdir, 0755 & ~ umask() );
}
/**
* This declaration throws a "Strict standards" warning in PHP 5.6.
* @todo: Remove the comment when we drop support for PHP 5.6.
*/
//abstract public static function getSubdir();
}

View File

@@ -0,0 +1,73 @@
<?php
namespace WPML\ST\MO\Generate;
use wpdb;
use WPML\Collect\Support\Collection;
use function WPML\Container\make;
use WPML\ST\TranslationFile\Domains;
use function wpml_collect;
use WPML_Locale;
class DomainsAndLanguagesRepository {
/** @var wpdb */
private $wpdb;
/** @var Domains */
private $domains;
/** @var WPML_Locale */
private $locale;
/**
* @param wpdb $wpdb
* @param Domains $domains
* @param WPML_Locale $wp_locale
*/
public function __construct( wpdb $wpdb, Domains $domains, WPML_Locale $wp_locale ) {
$this->wpdb = $wpdb;
$this->domains = $domains;
$this->locale = $wp_locale;
}
/**
* @return Collection
*/
public function get() {
return $this->getAllDomains()->map( function ( $row ) {
return (object) [
'domain' => $row->domain,
'locale' => $this->locale->get_locale( $row->languageCode )
];
} )->values();
}
/**
* @return Collection
*/
private function getAllDomains() {
$moDomains = $this->domains->getMODomains()->toArray();
if ( ! $moDomains ) {
return wpml_collect( [] );
}
$sql = "
SELECT DISTINCT (BINARY s.context) as `domain`, st.language as `languageCode`
FROM {$this->wpdb->prefix}icl_string_translations st
INNER JOIN {$this->wpdb->prefix}icl_strings s ON s.id = st.string_id
WHERE st.`status` = 10 AND ( st.`value` != st.mo_string OR st.mo_string IS NULL)
AND s.context IN(" . wpml_prepare_in( $moDomains ) . ")
";
$result = $this->wpdb->get_results( $sql );
return wpml_collect( $result );
}
/**
* @return bool
*/
public static function hasTranslationFilesTable() {
return make( \WPML_Upgrade_Schema::class )->does_table_exist( 'icl_mo_files_domains' );
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace WPML\ST\MO\Generate;
use WPML\ST\MO\File\Builder;
use WPML\ST\MO\File\makeDir;
use WPML\ST\MO\Hooks\LoadMissingMOFiles;
use WPML\ST\TranslationFile\StringsRetrieve;
use WPML\WP\OptionManager;
use function WPML\Container\make;
class MissingMOFile {
use makeDir;
const OPTION_GROUP = 'ST-MO';
const OPTION_NAME = 'missing-mo-processed';
/**
* @var Builder
*/
private $builder;
/**
* @var StringsRetrieve
*/
private $stringsRetrieve;
/**
* @var \WPML_Language_Records
*/
private $languageRecords;
/**
* @var OptionManager
*/
private $optionManager;
public function __construct(
\WP_Filesystem_Direct $filesystem,
Builder $builder,
StringsRetrieveMOOriginals $stringsRetrieve,
\WPML_Language_Records $languageRecords,
OptionManager $optionManager
) {
$this->filesystem = $filesystem;
$this->builder = $builder;
$this->stringsRetrieve = $stringsRetrieve;
$this->languageRecords = $languageRecords;
$this->optionManager = $optionManager;
}
/**
* @param string $generateMoPath
* @param string $domain
*/
public function run( $generateMoPath, $domain ) {
$processed = $this->getProcessed();
if ( ! $processed->contains( basename( $generateMoPath ) ) && $this->maybeCreateSubdir() ) {
$locale = make( \WPML_ST_Translations_File_Locale::class )->get( $generateMoPath, $domain );
$strings = $this->stringsRetrieve->get(
$domain,
$this->languageRecords->get_language_code( $locale ),
false
);
if ( ! empty( $strings ) ) {
$fileContents = $this->builder
->set_language( $locale )
->get_content( $strings );
$this->filesystem->put_contents( $generateMoPath, $fileContents, 0755 & ~umask() );
}
$processed->push( $generateMoPath );
$this->optionManager->set( self::OPTION_GROUP, self::OPTION_NAME, $processed->toArray() );
}
}
public function isNotProcessed( $generateMoPath ) {
return ! $this->getProcessed()->contains( basename($generateMoPath) );
}
public static function getSubdir() {
return WP_LANG_DIR . LoadMissingMOFiles::MISSING_MO_FILES_DIR;
}
/**
* @return \WPML\Collect\Support\Collection
*/
private function getProcessed() {
return wpml_collect( $this->optionManager->get( self::OPTION_GROUP, self::OPTION_NAME, [] ) )
->map( function ( $path ) {
return basename( $path );
} );
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace WPML\ST\MO\Generate\MultiSite;
class Condition {
/**
* @return bool
*/
public function shouldRunWithAllSites() {
return is_multisite() && (
$this->hasPostBodyParam()
|| is_super_admin()
|| defined( 'WP_CLI' )
);
}
private function hasPostBodyParam() {
$request_body = file_get_contents( 'php://input' );
$data = filter_var_array( (array)json_decode( $request_body ), FILTER_SANITIZE_STRING );
return isset( $data['runForAllSites'] ) && $data['runForAllSites'];
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace WPML\ST\MO\Generate\MultiSite;
class Executor {
const MAIN_SITE_ID = 1;
/**
* @param callable $callback
*
* @return \WPML\Collect\Support\Collection
*/
public function withEach( $callback ) {
$applyCallback = function( $siteId ) use ( $callback ) {
switch_to_blog( $siteId );
return [ $siteId, $callback() ];
};
$initialBlogId = get_current_blog_id();
$result = $this->getSiteIds()->map( $applyCallback );
switch_to_blog( $initialBlogId );
return $result;
}
/**
* @return \WPML\Collect\Support\Collection
*/
public function getSiteIds() {
return \wpml_collect( get_sites( [ 'number' => PHP_INT_MAX ] ) )->pluck( 'id' );
}
/**
* @param int $siteId
* @param callable $callback
*
* @return mixed
*/
public function executeWith( $siteId, callable $callback ) {
switch_to_blog( $siteId );
$result = $callback();
restore_current_blog();
return $result;
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace WPML\ST\MO\Generate\Process;
use WPML\Utils\Pager;
use WPML\ST\MO\Generate\MultiSite\Executor;
class MultiSiteProcess implements Process {
/** @var Executor */
private $multiSiteExecutor;
/** @var SingleSiteProcess */
private $singleSiteProcess;
/** @var Status */
private $status;
/** @var Pager */
private $pager;
/** @var SubSiteValidator */
private $subSiteValidator;
/**
* @param Executor $multiSiteExecutor
* @param SingleSiteProcess $singleSiteProcess
* @param Status $status
* @param Pager $pager
* @param SubSiteValidator $subSiteValidator
*/
public function __construct(
Executor $multiSiteExecutor,
SingleSiteProcess $singleSiteProcess,
Status $status,
Pager $pager,
SubSiteValidator $subSiteValidator
) {
$this->multiSiteExecutor = $multiSiteExecutor;
$this->singleSiteProcess = $singleSiteProcess;
$this->status = $status;
$this->pager = $pager;
$this->subSiteValidator = $subSiteValidator;
}
public function runAll() {
$this->multiSiteExecutor->withEach( $this->runIfSetupComplete( [ $this->singleSiteProcess, 'runAll' ] ) );
$this->status->markComplete( true );
}
/**
* @return int Is completed
*/
public function runPage() {
$remaining = $this->pager->iterate( $this->multiSiteExecutor->getSiteIds(), function ( $siteId ) {
return $this->multiSiteExecutor->executeWith(
$siteId,
$this->runIfSetupComplete( function () {
// no more remaining pages which means that process is done
return $this->singleSiteProcess->runPage() === 0;
} )
);
} );
if ( $remaining === 0 ) {
$this->multiSiteExecutor->executeWith( Executor::MAIN_SITE_ID, function () {
$this->status->markComplete( true );
} );
}
return $remaining;
}
/**
* @return int
*/
public function getPagesCount() {
$isCompletedForAllSites = $this->multiSiteExecutor->executeWith(
Executor::MAIN_SITE_ID,
[ $this->status, 'isCompleteForAllSites' ]
);
if ( $isCompletedForAllSites ) {
return 0;
}
return $this->multiSiteExecutor->getSiteIds()->count();
}
/**
* @return bool
*/
public function isCompleted() {
return $this->getPagesCount() === 0;
}
private function runIfSetupComplete( $callback ) {
return function () use ( $callback ) {
if ( $this->subSiteValidator->isValid() ) {
return $callback();
}
return true;
};
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace WPML\ST\MO\Generate\Process;
interface Process {
public function runAll();
/**
* @return int Remaining
*/
public function runPage();
/**
* @return int
*/
public function getPagesCount();
/**
* @return bool
*/
public function isCompleted();
}

View File

@@ -0,0 +1,70 @@
<?php
namespace WPML\ST\MO\Generate\Process;
use WPML\ST\MO\File\ManagerFactory;
use WPML\ST\MO\Generate\MultiSite\Condition;
use WPML\Utils\Pager;
use function WPML\Container\make;
class ProcessFactory {
const FILES_PAGER = 'wpml-st-mo-generate-files-pager';
const FILES_PAGE_SIZE = 20;
const SITES_PAGER = 'wpml-st-mo-generate-sites-pager';
/** @var Condition */
private $multiSiteCondition;
/**
* @param Condition $multiSiteCondition
*/
public function __construct( Condition $multiSiteCondition = null ) {
$this->multiSiteCondition = $multiSiteCondition ?: new Condition();
}
/**
* @return Process
* @throws \WPML\Auryn\InjectionException
*/
public function create() {
$singleSiteProcess = self::createSingle();
if ( $this->multiSiteCondition->shouldRunWithAllSites() ) {
return make( MultiSiteProcess::class,
[ ':singleSiteProcess' => $singleSiteProcess, ':pager' => new Pager( self::SITES_PAGER, 1 ) ]
);
} else {
return $singleSiteProcess;
}
}
/**
* @param bool $isBackgroundProcess
*
* @return SingleSiteProcess
* @throws \WPML\Auryn\InjectionException
*/
public static function createSingle( $isBackgroundProcess = false ) {
return make(
SingleSiteProcess::class,
[
':pager' => new Pager( self::FILES_PAGER, self::FILES_PAGE_SIZE ),
':manager' => ManagerFactory::create(),
':migrateAdminTexts' => \WPML_Admin_Texts::get_migrator(),
':status' => self::createStatus( $isBackgroundProcess ),
]
);
}
/**
* @param bool $isBackgroundProcess
*
* @return mixed|\Mockery\MockInterface|Status
* @throws \WPML\Auryn\InjectionException
*/
public static function createStatus( $isBackgroundProcess = false ) {
return make( Status::class, [
':optionPrefix' => $isBackgroundProcess ? Status::class . '_background' : null
] );
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace WPML\ST\MO\Generate\Process;
use WPML\ST\MO\File\Manager;
use WPML\ST\MO\Generate\DomainsAndLanguagesRepository;
use WPML\Utils\Pager;
class SingleSiteProcess implements Process {
CONST TIMEOUT = 5;
/** @var DomainsAndLanguagesRepository */
private $domainsAndLanguagesRepository;
/** @var Manager */
private $manager;
/** @var Status */
private $status;
/** @var Pager */
private $pager;
/** @var callable */
private $migrateAdminTexts;
/**
* @param DomainsAndLanguagesRepository $domainsAndLanguagesRepository
* @param Manager $manager
* @param Status $status
* @param Pager $pager
* @param callable $migrateAdminTexts
*/
public function __construct(
DomainsAndLanguagesRepository $domainsAndLanguagesRepository,
Manager $manager,
Status $status,
Pager $pager,
callable $migrateAdminTexts
) {
$this->domainsAndLanguagesRepository = $domainsAndLanguagesRepository;
$this->manager = $manager;
$this->status = $status;
$this->pager = $pager;
$this->migrateAdminTexts = $migrateAdminTexts;
}
public function runAll() {
call_user_func( $this->migrateAdminTexts );
$this->getDomainsAndLanguages()->each( function ( $row ) {
$this->manager->add( $row->domain, $row->locale );
} );
$this->status->markComplete();
}
/**
* @return int Remaining
*/
public function runPage() {
if ( $this->pager->getProcessedCount() === 0 ) {
call_user_func( $this->migrateAdminTexts );
}
$domains = $this->getDomainsAndLanguages();;
$remaining = $this->pager->iterate( $domains, function ( $row ) {
$this->manager->add( $row->domain, $row->locale );
return true;
}, self::TIMEOUT );
if ( $remaining === 0 ) {
$this->status->markComplete();
}
return $remaining;
}
public function getPagesCount() {
if ( $this->status->isComplete() ) {
return 0;
}
$domains = $this->getDomainsAndLanguages();
if ( $domains->count() === 0 ) {
$this->status->markComplete();
}
return $domains->count();
}
private function getDomainsAndLanguages() {
return DomainsAndLanguagesRepository::hasTranslationFilesTable()
? $this->domainsAndLanguagesRepository->get()
: wpml_collect();
}
/**
* @return bool
*/
public function isCompleted() {
return $this->getPagesCount() === 0;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace WPML\ST\MO\Generate\Process;
class Status {
/** @var \SitePress */
private $sitepress;
/** @var string */
private $optionPrefix;
/**
* @param \SitePress $sitepress
* @param string|null $optionPrefix
*/
public function __construct( \SitePress $sitepress, $optionPrefix = null ) {
$this->sitepress = $sitepress;
$this->optionPrefix = $optionPrefix ?: self::class;
}
/**
* @param bool $allSites
*/
public function markComplete( $allSites = false ) {
$settings = $this->sitepress->get_setting( 'st', [] );
$settings[ $this->getOptionName( $allSites ) ] = true;
$this->sitepress->set_setting( 'st', $settings, true );
}
/**
* @param bool $allSites
*/
public function markIncomplete( $allSites = false ) {
$settings = $this->sitepress->get_setting( 'st', [] );
unset( $settings[ $this->getOptionName( $allSites ) ] );
$this->sitepress->set_setting( 'st', $settings, true );
}
public function markIncompleteForAll() {
$this->markIncomplete( true );
}
/**
* @return bool
*/
public function isComplete() {
$st_settings = $this->sitepress->get_setting( 'st', [] );
return isset( $st_settings[ $this->getOptionName( false ) ] );
}
/**
* @return bool
*/
public function isCompleteForAllSites() {
$st_settings = $this->sitepress->get_setting( 'st', [] );
return isset( $st_settings[ $this->getOptionName( true ) ] );
}
private function getOptionName( $allSites ) {
return $allSites ? $this->optionPrefix . '_has_run_all_sites' : $this->optionPrefix . '_has_run';
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace WPML\ST\MO\Generate\Process;
use function WPML\Container\make;
class SubSiteValidator {
/**
* @return bool
*/
public function isValid() {
global $sitepress;
return $sitepress->is_setup_complete() && $this->hasTranslationFilesTable();
}
/**
* @return bool
*/
private function hasTranslationFilesTable() {
return make( \WPML_Upgrade_Schema::class )->does_table_exist( 'icl_mo_files_domains' );
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace WPML\ST\MO\Generate;
use WPML\ST\TranslationFile\StringsRetrieve;
class StringsRetrieveMOOriginals extends StringsRetrieve {
/**
* @param array $row_data
*
* @return string|null
*/
public static function parseTranslation( array $row_data ) {
return ! empty( $row_data['mo_string'] ) ? $row_data['mo_string'] : null;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace WPML\ST\MO\Hooks;
use WPML\FP\Lst;
use WPML\ST\MO\File\Manager;
use WPML\ST\MO\JustInTime\MO;
use WPML\ST\MO\LoadedMODictionary;
use WPML\ST\TranslationFile\Domains;
use function WPML\FP\curryN;
use function WPML\FP\partial;
use function WPML\FP\pipe;
use function WPML\FP\spreadArgs;
class CustomTextDomains implements \IWPML_Action {
/** @var Manager $manager */
private $manager;
/** @var Domains $domains */
private $domains;
/** @var LoadedMODictionary $loadedDictionary */
private $loadedDictionary;
/** @var callable */
private $syncMissingFile;
public function __construct(
Manager $file_manager,
Domains $domains,
LoadedMODictionary $loadedDictionary,
callable $syncMissingFile = null
) {
$this->manager = $file_manager;
$this->domains = $domains;
$this->loadedDictionary = $loadedDictionary;
$this->syncMissingFile = $syncMissingFile ?: function () {
};
}
public function add_hooks() {
$locale = get_locale();
$getDomainPathTuple = function ( $domain ) use ( $locale ) {
return [ $domain, $this->manager->getFilepath( $domain, $locale ) ];
};
$isReadableFile = function ( $domainAndFilePath ) {
return is_readable( $domainAndFilePath[1] );
};
$addJitMoToL10nGlobal = pipe( Lst::nth( 0 ), function ( $domain ) use ( $locale ) {
$GLOBALS['l10n'][ $domain ] = new MO( $this->loadedDictionary, $locale, $domain );
} );
\wpml_collect( $this->domains->getCustomMODomains() )
->map( $getDomainPathTuple )
->each( spreadArgs( $this->syncMissingFile ) )
->each( spreadArgs( [ $this->loadedDictionary, 'addFile' ] ) )
->filter( $isReadableFile )
->each( $addJitMoToL10nGlobal );
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace WPML\ST\MO\Hooks;
use WPML\ST\Gettext\Settings;
class DetectPrematurelyTranslatedStrings implements \IWPML_Action {
/** @var string[] */
private $domains = [];
/** @var string[] */
private $preloadedDomains = [];
/** @var \SitePress */
private $sitepress;
/** @var Settings */
private $gettextHooksSettings;
/**
* @param \SitePress $sitepress
*/
public function __construct( \SitePress $sitepress, Settings $settings ) {
$this->sitepress = $sitepress;
$this->gettextHooksSettings = $settings;
}
/**
* Init gettext hooks.
*/
public function add_hooks() {
if ( $this->gettextHooksSettings->isAutoRegistrationEnabled() ) {
$domains = $this->sitepress->get_setting( 'gettext_theme_domain_name' );
$this->preloadedDomains = array_filter( array_map( 'trim', explode( ',', $domains ) ) );
add_filter( 'gettext', [ $this, 'gettext_filter' ], 9, 3 );
add_filter( 'gettext_with_context', [ $this, 'gettext_with_context_filter' ], 1, 4 );
add_filter( 'ngettext', [ $this, 'ngettext_filter' ], 9, 5 );
add_filter( 'ngettext_with_context', [ $this, 'ngettext_with_context_filter' ], 9, 6 );
add_filter( 'override_load_textdomain', [ $this, 'registerDomainToPreloading' ], 10, 2 );
}
}
/**
* @param string $translation
* @param string $text
* @param string|array $domain
*
* @return string
*/
public function gettext_filter( $translation, $text, $domain ) {
$this->registerDomain( $domain );
return $translation;
}
/**
* @param string $translation
* @param string $text
* @param string $context
* @param string $domain
*
* @return string
*/
public function gettext_with_context_filter( $translation, $text, $context, $domain ) {
$this->registerDomain( $domain );
return $translation;
}
/**
* @param string $translation
* @param string $single
* @param string $plural
* @param string $number
* @param string|array $domain
*
* @return string
*/
public function ngettext_filter( $translation, $single, $plural, $number, $domain ) {
$this->registerDomain( $domain );
return $translation;
}
/**
* @param string $translation
* @param string $single
* @param string $plural
* @param string $number
* @param string $context
* @param string $domain
*
* @return string
*
*/
public function ngettext_with_context_filter( $translation, $single, $plural, $number, $context, $domain ) {
$this->registerDomain( $domain );
return $translation;
}
private function registerDomain( $domain ) {
if ( ! in_array( $domain, $this->preloadedDomains ) ) {
$this->domains[ $domain ] = true;
}
}
public function registerDomainToPreloading( $plugin_override, $domain ) {
if ( array_key_exists( $domain, $this->domains ) && ! in_array( $domain, $this->preloadedDomains, true ) ) {
$this->preloadedDomains[] = $domain;
$this->sitepress->set_setting(
'gettext_theme_domain_name',
implode( ',', array_unique( $this->preloadedDomains ) )
);
$this->sitepress->save_settings();
}
return $plugin_override;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace WPML\ST\MO\Hooks;
use IWPML_Action;
use WPML\ST\DB\Mappers\DomainsRepository;
use WPML\ST\MO\File\ManagerFactory;
use WPML\ST\TranslationFile\Sync\FileSync;
use WPML\ST\TranslationFile\UpdateHooksFactory;
use WPML\ST\TranslationFile\Hooks;
use function WPML\Container\make;
class Factory implements \IWPML_Backend_Action_Loader, \IWPML_Frontend_Action_Loader {
/**
* Create hooks.
*
* @return IWPML_Action[]
* @throws \WPML\Auryn\InjectionException Auryn Exception.
*/
public function create() {
$manager = ManagerFactory::create();
$moFileSync = make(
Sync::class,
[
':fileSync' => make( FileSync::class, [ ':manager' => ManagerFactory::create() ] ),
':useFileSynchronization' => [ Hooks::class, 'useFileSynchronization' ],
]
);
return [
UpdateHooksFactory::create(),
make( LoadTextDomain::class, [ ':file_manager' => $manager ] ),
make( CustomTextDomains::class, [
':file_manager' => $manager,
':syncMissingFile' => [ $moFileSync, 'syncFile' ],
] ),
make( LanguageSwitch::class ),
make( LoadMissingMOFiles::class ),
make( PreloadThemeMoFile::class ),
make( DetectPrematurelyTranslatedStrings::class ),
$moFileSync,
make( StringsLanguageChanged::class, [
':manager' => $manager,
':getDomainsByStringIds' => DomainsRepository::getByStringIds(),
] ),
];
}
}

View File

@@ -0,0 +1,163 @@
<?php
namespace WPML\ST\MO\Hooks;
use WPML\ST\MO\JustInTime\MOFactory;
use WPML\ST\MO\WPLocaleProxy;
use WPML\ST\Utils\LanguageResolution;
class LanguageSwitch implements \IWPML_Action {
/** @var MOFactory $jit_mo_factory */
private $jit_mo_factory;
/** @var LanguageResolution $language_resolution */
private $language_resolution;
/** @var null|string $current_locale */
private static $current_locale;
/** @var array $globals_cache */
private static $globals_cache = [];
public function __construct(
LanguageResolution $language_resolution,
MOFactory $jit_mo_factory
) {
$this->language_resolution = $language_resolution;
$this->jit_mo_factory = $jit_mo_factory;
}
public function add_hooks() {
add_action( 'wpml_language_has_switched', [ $this, 'languageHasSwitched' ] );
}
/** @param string $locale */
private function setCurrentLocale( $locale ) {
self::$current_locale = $locale;
}
/** @return string */
public function getCurrentLocale() {
return self::$current_locale;
}
public function languageHasSwitched() {
$this->initCurrentLocale();
$new_locale = $this->language_resolution->getCurrentLocale();
$this->switchToLocale( $new_locale );
}
public function initCurrentLocale() {
if ( ! $this->getCurrentLocale() ) {
add_filter( 'locale', [ $this, 'filterLocale' ], PHP_INT_MAX );
$this->setCurrentLocale( $this->language_resolution->getCurrentLocale() );
}
}
/**
* This method will act as the WP Core function `switch_to_locale`,
* but in a more efficient way. It will avoid to instantly load
* the domains loaded in the previous locale. Instead, it will let
* the domains be loaded via the "just in time" function.
*
* @param string $new_locale
*/
public function switchToLocale( $new_locale ) {
if ( $new_locale === $this->getCurrentLocale() ) {
return;
}
$this->updateCurrentGlobalsCache();
$this->changeWpLocale( $new_locale );
$this->changeMoObjects( $new_locale );
$this->setCurrentLocale( $new_locale );
}
/**
* @param string|null $locale
*/
public static function resetCache( $locale = null ) {
self::$current_locale = $locale;
self::$globals_cache = [];
}
/**
* We need to take a new copy of the current locale globals
* because some domains could have been added with the "just in time"
* mechanism.
*/
private function updateCurrentGlobalsCache() {
$cache = [
'wp_locale' => isset( $GLOBALS['wp_locale'] ) ? $GLOBALS['wp_locale'] : null,
'l10n' => isset( $GLOBALS['l10n'] ) ? (array) $GLOBALS['l10n'] : [],
];
self::$globals_cache[ $this->getCurrentLocale() ] = $cache;
}
/**
* @param string $new_locale
*/
private function changeWpLocale( $new_locale ) {
if ( isset( self::$globals_cache[ $new_locale ]['wp_locale'] ) ) {
$GLOBALS['wp_locale'] = self::$globals_cache[ $new_locale ]['wp_locale'];
} else {
/**
* WPLocaleProxy is a wrapper of \WP_Locale with a kind of lazy initialization
* to avoid loading the default domain for strings that
* we don't use in this transitory language.
*/
$GLOBALS['wp_locale'] = new WPLocaleProxy();
}
}
/**
* @param string $new_locale
*/
private function changeMoObjects( $new_locale ) {
$this->resetTranslationAvailabilityInformation();
$cachedMoObjects = isset( self::$globals_cache[ $new_locale ]['l10n'] )
? self::$globals_cache[ $new_locale ]['l10n']
: [];
/**
* The JustInTimeMO objects will replaced themselves on the fly
* by the legacy default MO object if a string is translated.
* This is because the function "_load_textdomain_just_in_time"
* does not support the default domain and MO files outside the
* "wp-content/languages" folder.
*/
$GLOBALS['l10n'] = $this->jit_mo_factory->get( $new_locale, $this->getUnloadedDomains(), $cachedMoObjects );
}
private function resetTranslationAvailabilityInformation() {
global $wp_textdomain_registry;
if ( ! isset( $wp_textdomain_registry ) && function_exists( '_get_path_to_translation' ) ) {
_get_path_to_translation( null, true );
}
}
/**
* @param string $locale
*
* @return string
*/
public function filterLocale( $locale ) {
$currentLocale = $this->getCurrentLocale();
if ( $currentLocale ) {
return $currentLocale;
}
return $locale;
}
/**
* @return array
*/
private function getUnloadedDomains() {
return isset( $GLOBALS['l10n_unloaded'] ) ? array_keys( (array) $GLOBALS['l10n_unloaded'] ) : [];
}
}

View File

@@ -0,0 +1,170 @@
<?php
namespace WPML\ST\MO\Hooks;
use WPML\Collect\Support\Collection;
use WPML\ST\MO\Generate\MissingMOFile;
use WPML\WP\OptionManager;
use function WPML\Container\make;
class LoadMissingMOFiles implements \IWPML_Action {
const MISSING_MO_FILES_DIR = '/wpml/missing/';
const OPTION_GROUP = 'ST-MO';
const MISSING_MO_OPTION = 'missing-mo';
const TIMEOUT = 10;
const WPML_VERSION_INTRODUCING_ST_MO_FLOW = '4.3.0';
/**
* @var MissingMOFile
*/
private $generateMissingMoFile;
/**
* @var OptionManager
*/
private $optionManager;
/** @var \WPML_ST_Translations_File_Dictionary_Storage_Table */
private $moFilesDictionary;
public function __construct(
MissingMOFile $generateMissingMoFile,
OptionManager $optionManager,
\WPML_ST_Translations_File_Dictionary_Storage_Table $moFilesDictionary
) {
$this->generateMissingMoFile = $generateMissingMoFile;
$this->optionManager = $optionManager;
$this->moFilesDictionary = $moFilesDictionary;
}
public function add_hooks() {
if ( $this->wasWpmlInstalledPriorToMoFlowChanges() ) {
add_filter( 'load_textdomain_mofile', [ $this, 'recordMissing' ], 10, 2 );
add_action( 'shutdown', [ $this, 'generateMissing' ] );
}
}
/**
* @param string $mofile
* @param string $domain
*
* @return string
*/
public function recordMissing( $mofile, $domain ) {
if ( strpos( $mofile, WP_LANG_DIR . '/themes/' ) === 0 ) {
return $mofile;
}
if ( strpos( $mofile, WP_LANG_DIR . '/plugins/' ) === 0 ) {
return $mofile;
}
$missing = $this->getMissing();
if ( self::isReadable( $mofile ) ) {
if ( $missing->has( $domain ) ) {
$this->saveMissing( $missing->forget( $domain ) );
}
return $mofile;
}
if ( ! $this->moFilesDictionary->find( $mofile ) ) {
return $mofile;
}
$generatedFile = $this->getGeneratedFileName( $mofile, $domain );
if ( self::isReadable( $generatedFile ) ) {
return $generatedFile;
}
if ( $this->generateMissingMoFile->isNotProcessed( $generatedFile ) ) {
$this->saveMissing( $missing->put( $domain, $mofile ) );
}
return $mofile;
}
public function generateMissing() {
$lock = make( 'WPML\Utilities\Lock', [ ':name' => self::class ] );
$missing = $this->getMissing();
if ( $missing->count() && $lock->create() ) {
$generate = function ( $pair ) {
list( $domain, $mofile ) = $pair;
$generatedFile = $this->getGeneratedFileName( $mofile, $domain );
$this->generateMissingMoFile->run( $generatedFile, $domain );
};
$unProcessed = $missing->assocToPair()
->eachWithTimeout( $generate, self::getTimeout() )
->pairToAssoc();
$this->saveMissing( $unProcessed );
$lock->release();
}
}
public static function isReadable( $mofile ) {
return is_readable( $mofile );
}
/**
* @return \WPML\Collect\Support\Collection
*/
private function getMissing() {
return wpml_collect( $this->optionManager->get( self::OPTION_GROUP, self::MISSING_MO_OPTION, [] ) );
}
/**
* @param \WPML\Collect\Support\Collection $missing
*/
private function saveMissing( \WPML\Collect\Support\Collection $missing ) {
$this->optionManager->set( self::OPTION_GROUP, self::MISSING_MO_OPTION, $missing->toArray() );
}
public static function getTimeout() {
return self::TIMEOUT;
}
/**
* @return bool
*/
private function wasWpmlInstalledPriorToMoFlowChanges() {
$wpml_start_version = \get_option( \WPML_Installation::WPML_START_VERSION_KEY, '0.0.0' );
return version_compare( $wpml_start_version, self::WPML_VERSION_INTRODUCING_ST_MO_FLOW, '<' );
}
/**
* @param string $mofile
* @param string $domain
*
* @return string
*/
private function getGeneratedFileName( $mofile, $domain ) {
$fileName = basename( $mofile );
if ( $this->isNonDefaultWithMissingDomain( $fileName, $domain ) ) {
$fileName = $domain . '-' . $fileName;
}
return WP_LANG_DIR . self::MISSING_MO_FILES_DIR . $fileName;
}
/**
* There's a fallback for theme that is looking for
* this kind of file `wp-content/themes/hybrid/ru_RU.mo`.
* We need to add the domain otherwise it collides with
* the MO file for the default domain.
*
* @param string $fileName
* @param string $domain
*
* @return bool
*/
private function isNonDefaultWithMissingDomain( $fileName, $domain ) {
return 'default' !== $domain
&& preg_match( '/^[a-z]+_?[A-Z]*\.mo$/', $fileName );
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace WPML\ST\MO\Hooks;
use WPML\ST\MO\File\Manager;
use WPML\ST\MO\LoadedMODictionary;
use WPML_ST_Translations_File_Locale;
use function WPML\FP\partial;
class LoadTextDomain implements \IWPML_Action {
const PRIORITY_OVERRIDE = 10;
/** @var Manager $file_manager */
private $file_manager;
/** @var WPML_ST_Translations_File_Locale $file_locale */
private $file_locale;
/** @var LoadedMODictionary $loaded_mo_dictionary */
private $loaded_mo_dictionary;
/** @var array $loaded_domains */
private $loaded_domains = [];
public function __construct(
Manager $file_manager,
WPML_ST_Translations_File_Locale $file_locale,
LoadedMODictionary $loaded_mo_dictionary
) {
$this->file_manager = $file_manager;
$this->file_locale = $file_locale;
$this->loaded_mo_dictionary = $loaded_mo_dictionary;
}
public function add_hooks() {
$this->reloadAlreadyLoadedMOFiles();
add_filter( 'override_load_textdomain', [ $this, 'overrideLoadTextDomain' ], 10, 3 );
add_filter( 'override_unload_textdomain', [ $this, 'overrideUnloadTextDomain' ], 10, 2 );
add_action( 'wpml_language_has_switched', [ $this, 'languageHasSwitched' ] );
}
/**
* When a MO file is loaded, we override the process to load
* the custom MO file before.
*
* That way, the custom MO file will be merged into the subsequent
* native MO files and the custom MO translations will always
* overwrite the native ones.
*
* This gives us the ability to build partial custom MO files
* with only the modified translations.
*
* @param bool $override Whether to override the .mo file loading. Default false.
* @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @param string $mofile Path to the MO file.
*
* @return bool
*/
public function overrideLoadTextDomain( $override, $domain, $mofile ) {
if ( ! $mofile ) {
return $override;
}
if ( ! $this->isCustomMOLoaded( $domain ) ) {
remove_filter( 'override_load_textdomain', [ $this, 'overrideLoadTextDomain' ], 10 );
$locale = $this->file_locale->get( $mofile, $domain );
$this->loadCustomMOFile( $domain, $mofile, $locale );
add_filter( 'override_load_textdomain', [ $this, 'overrideLoadTextDomain' ], 10, 3 );
}
$this->loaded_mo_dictionary->addFile( $domain, $mofile );
return $override;
}
/**
* @param bool $override
* @param string $domain
*
* @return bool
*/
public function overrideUnloadTextDomain( $override, $domain ) {
$key = array_search( $domain, $this->loaded_domains );
if ( false !== $key ) {
unset( $this->loaded_domains[ $key ] );
}
return $override;
}
/**
* @param string $domain
*
* @return bool
*/
private function isCustomMOLoaded( $domain ) {
return in_array( $domain, $this->loaded_domains, true );
}
private function loadCustomMOFile( $domain, $mofile, $locale ) {
$wpml_mofile = $this->file_manager->get( $domain, $locale );
if ( $wpml_mofile && $wpml_mofile !== $mofile ) {
load_textdomain( $domain, $wpml_mofile );
}
$this->setCustomMOLoaded( $domain );
}
private function reloadAlreadyLoadedMOFiles() {
$this->loaded_mo_dictionary->getEntities()->each( function ( $entity ) {
unload_textdomain( $entity->domain );
$locale = $this->file_locale->get( $entity->mofile, $entity->domain );
$this->loadCustomMOFile( $entity->domain, $entity->mofile, $locale );
load_textdomain( $entity->domain, $entity->mofile );
} );
}
/**
* @param string $domain
*/
private function setCustomMOLoaded( $domain ) {
$this->loaded_domains[] = $domain;
}
public function languageHasSwitched() {
$this->loaded_domains = [];
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace WPML\ST\MO\Hooks;
use WPML\Collect\Support\Collection;
use WPML\ST\Gettext\AutoRegisterSettings;
use function WPML\Container\make;
class PreloadThemeMoFile implements \IWPML_Action {
const SETTING_KEY = 'theme_localization_load_textdomain';
const SETTING_DISABLED = 0;
const SETTING_ENABLED = 1;
const SETTING_ENABLED_FOR_LOAD_TEXT_DOMAIN = 2;
/** @var \SitePress */
private $sitepress;
/** @var \wpdb */
private $wpdb;
public function __construct( \SitePress $sitepress, \wpdb $wpdb ) {
$this->sitepress = $sitepress;
$this->wpdb = $wpdb;
}
public function add_hooks() {
$domainsSetting = $this->sitepress->get_setting( 'gettext_theme_domain_name' );
$domains = empty( $domainsSetting ) ? [] : explode( ',', $domainsSetting );
$domains = \wpml_collect( array_map( 'trim', $domains ) );
$loadTextDomainSetting = (int) $this->sitepress->get_setting( static::SETTING_KEY );
$isEnabled = $loadTextDomainSetting === static::SETTING_ENABLED;
if ( $loadTextDomainSetting === static::SETTING_ENABLED_FOR_LOAD_TEXT_DOMAIN ) {
/** @var AutoRegisterSettings $autoStrings */
$autoStrings = make( AutoRegisterSettings::class );
$isEnabled = $autoStrings->isEnabled();
}
if ( $isEnabled && $domains->count() ) {
$this->getMOFilesByDomainsAndLocale( $domains, get_locale() )->map( function ( $fileResult ) {
load_textdomain( $fileResult->domain, $fileResult->file_path );
} );
}
}
/**
* @param Collection<string> $domains
* @param string $locale
*
* @return Collection
*/
private function getMOFilesByDomainsAndLocale( $domains, $locale ) {
$domainsClause = wpml_prepare_in( $domains->toArray(), '%s' );
$sql = "
SELECT file_path, domain
FROM {$this->wpdb->prefix}icl_mo_files_domains
WHERE domain IN ({$domainsClause}) AND file_path REGEXP %s
";
$sql = $this->wpdb->prepare(
$sql,
'((\\/|-)' . $locale . '(\\.|-))+'
);
return \wpml_collect( $this->wpdb->get_results( $sql ) );
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace WPML\ST\MO\Hooks;
use WPML\FP\Fns;
use WPML\FP\Lst;
use WPML\FP\Obj;
use WPML\ST\MO\File\Manager;
use WPML\ST\MO\Generate\DomainsAndLanguagesRepository;
use function WPML\FP\pipe;
class StringsLanguageChanged implements \IWPML_Action {
private $domainsAndLanguageRepository;
private $manager;
private $getDomainsByStringIds;
/**
* @param DomainsAndLanguagesRepository $domainsAndLanguageRepository
* @param Manager $manager
* @param callable $getDomainsByStringIds
*/
public function __construct(
DomainsAndLanguagesRepository $domainsAndLanguageRepository,
Manager $manager,
callable $getDomainsByStringIds
) {
$this->domainsAndLanguageRepository = $domainsAndLanguageRepository;
$this->manager = $manager;
$this->getDomainsByStringIds = $getDomainsByStringIds;
}
public function add_hooks() {
add_action( 'wpml_st_language_of_strings_changed', [ $this, 'regenerateMOFiles' ] );
}
public function regenerateMOFiles( array $strings ) {
$stringDomains = call_user_func( $this->getDomainsByStringIds, $strings );
$this->domainsAndLanguageRepository
->get()
->filter( pipe( Obj::prop( 'domain' ), Lst::includes( Fns::__, $stringDomains ) ) )
->each( function ( $domainLangPair ) {
$this->manager->add( $domainLangPair->domain, $domainLangPair->locale );
} );
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace WPML\ST\MO\Hooks;
use WPML\ST\TranslationFile\Sync\FileSync;
class Sync implements \IWPML_Frontend_Action, \IWPML_Backend_Action, \IWPML_DIC_Action {
/** @var FileSync */
private $fileSync;
/** @var callable */
private $useFileSynchronization;
public function __construct( FileSync $fileSync, callable $useFileSynchronization ) {
$this->fileSync = $fileSync;
$this->useFileSynchronization = $useFileSynchronization;
}
public function add_hooks() {
if ( call_user_func( $this->useFileSynchronization ) ) {
add_filter(
'override_load_textdomain',
[ $this, 'syncCustomMoFileOnLoadTextDomain' ],
LoadTextDomain::PRIORITY_OVERRIDE - 1,
3
);
}
}
public function syncFile( $domain, $moFile ) {
if ( call_user_func( $this->useFileSynchronization ) ) {
$this->fileSync->sync( $moFile, $domain );
}
}
/**
* @param bool $override
* @param string $domain
* @param string $moFile
*
* @return bool
*/
public function syncCustomMoFileOnLoadTextDomain( $override, $domain, $moFile ) {
$this->fileSync->sync( $moFile, $domain );
return $override;
}
}

View File

@@ -0,0 +1,19 @@
<?php
/**
* @author OnTheGo Systems
*/
namespace WPML\ST\MO\JustInTime;
use WPML\ST\MO\LoadedMODictionary;
class DefaultMO extends MO {
public function __construct( LoadedMODictionary $loaded_mo_dictionary, $locale ) {
parent::__construct( $loaded_mo_dictionary, $locale, 'default' );
}
protected function loadTextDomain() {
load_default_textdomain( $this->locale );
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace WPML\ST\MO\JustInTime;
use NOOP_Translations;
use WPML\ST\MO\LoadedMODictionary;
class MO extends \MO {
/** @var LoadedMODictionary $loaded_mo_dictionary */
private $loaded_mo_dictionary;
/** @var string $locale */
protected $locale;
/** @var string $domain */
private $domain;
/** @var bool $isLoading */
private $isLoading = false;
/**
* @param LoadedMODictionary $loaded_mo_dictionary
* @param string $locale
* @param string $domain
*/
public function __construct(
LoadedMODictionary $loaded_mo_dictionary,
$locale,
$domain
) {
$this->loaded_mo_dictionary = $loaded_mo_dictionary;
$this->locale = $locale;
$this->domain = $domain;
}
/**
* @param string $singular
* @param string $context
*
* @return string
*/
public function translate( $singular, $context = null ) {
if ( $this->isLoading ) {
return $singular;
}
$this->load();
return _x( $singular, $context, $this->domain );
}
/**
* @param string $singular
* @param string $plural
* @param int $count
* @param string $context
*
* @return string
*/
public function translate_plural( $singular, $plural, $count, $context = null ) {
if ( $this->isLoading ) {
return $count > 1 ? $plural : $singular;
}
$this->load();
return _nx( $singular, $plural, $count, $context, $this->domain );
}
private function load() {
$this->isLoading = true;
$this->loadTextDomain();
if ( ! $this->isLoaded() ) {
/**
* If we could not load at least one MO file,
* we need to assign the domain with a `NOOP_Translations`
* object on the 'l10n' global.
* This will prevent recursive loop on the current object.
*/
$GLOBALS['l10n'][ $this->domain ] = new NOOP_Translations();
}
$this->isLoading = false;
}
protected function loadTextDomain() {
$this->loaded_mo_dictionary
->getFiles( $this->domain, $this->locale )
->each( function( $mofile ) {
load_textdomain( $this->domain, $mofile );
} );
}
/**
* In some cases, themes or plugins are hooking on
* `override_load_textdomain` so that the function
* `load_textdomain` always returns `true` even
* if the domain is not set on the global `$l10n`.
*
* That's why we need to check on the global `$l10n`.
*
* @return bool
*/
private function isLoaded() {
return isset( $GLOBALS['l10n'][ $this->domain ] )
&& ! $GLOBALS['l10n'][ $this->domain ] instanceof self;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace WPML\ST\MO\JustInTime;
use WPML\ST\MO\LoadedMODictionary;
class MOFactory {
/** @var LoadedMODictionary $loaded_mo_dictionary */
private $loaded_mo_dictionary;
public function __construct( LoadedMODictionary $loaded_mo_dictionary ) {
$this->loaded_mo_dictionary = $loaded_mo_dictionary;
}
/**
* We need to rely on the loaded dictionary rather than `$GLOBALS['l10n]`
* because a domain could have been loaded in a language that
* does not have a MO file and so it won't be added to the `$GLOBALS['l10n]`.
*
* @param string $locale
* @param array $excluded_domains
* @param array $cachedMoObjects
*
* @return array
*/
public function get( $locale, array $excluded_domains, array $cachedMoObjects ) {
$mo_objects = [
'default' => isset( $cachedMoObjects['default'] )
? $cachedMoObjects['default']
: new DefaultMO( $this->loaded_mo_dictionary, $locale ),
];
$excluded_domains[] = 'default';
foreach ( $this->loaded_mo_dictionary->getDomains( $excluded_domains ) as $domain ) {
$mo_objects[ $domain ] = isset( $cachedMoObjects[ $domain ] )
? $cachedMoObjects[ $domain ]
: new MO( $this->loaded_mo_dictionary, $locale, $domain );
}
return $mo_objects;
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace WPML\ST\MO;
use MO;
use stdClass;
use WPML\Collect\Support\Collection;
class LoadedMODictionary {
const PATTERN_SEARCH_LOCALE = '#([-]?)([a-z]+[_A-Z]*)(\.mo)$#i';
const LOCALE_PLACEHOLDER = '{LOCALE}';
/** @var array */
private $domainsCache = [];
/** @var Collection $mo_files */
private $mo_files;
public function __construct() {
$this->mo_files = wpml_collect( [] );
$this->collectFilesAddedBeforeInstantiation();
}
private function collectFilesAddedBeforeInstantiation() {
if ( isset( $GLOBALS['l10n'] ) && is_array( $GLOBALS['l10n'] ) ) {
wpml_collect( $GLOBALS['l10n'] )->each(
function ( $mo, $domain ) {
if ( $mo instanceof MO ) {
$this->addFile( $domain, $mo->get_filename() );
}
}
);
}
}
/**
* @param string $domain
* @param string $mofile
*/
public function addFile( $domain, $mofile ) {
$mofile_pattern = preg_replace(
self::PATTERN_SEARCH_LOCALE,
'$1' . self::LOCALE_PLACEHOLDER . '$3',
$mofile,
1
);
$hash = md5( $domain . $mofile_pattern );
$entity = (object) [
'domain' => $domain,
'mofile_pattern' => $mofile_pattern,
'mofile' => $mofile,
];
$this->mo_files->put( $hash, $entity );
$this->domainsCache = [];
}
/**
* @param array $excluded
*
* @return array
*/
public function getDomains( array $excluded = [] ) {
$key = md5( implode( $excluded ) );
if ( isset( $this->domainsCache[ $key ] ) ) {
return $this->domainsCache[ $key ];
}
$domains = $this->mo_files
->reject( $this->excluded( $excluded ) )
->pluck( 'domain' )
->unique()->values()->toArray();
$this->domainsCache[ $key ] = $domains;
return $domains;
}
/**
* @param string $domain
* @param string $locale
*
* @return Collection
*/
public function getFiles( $domain, $locale ) {
return $this->mo_files
->filter( $this->byDomain( $domain ) )
->map( $this->getFile( $locale ) )
->values();
}
/**
* @return Collection
*/
public function getEntities() {
return $this->mo_files;
}
/**
* @param array $excluded
*
* @return \Closure
*/
private function excluded( array $excluded ) {
return function ( stdClass $entity ) use ( $excluded ) {
return in_array( $entity->domain, $excluded, true );
};
}
/**
* @param string $domain
*
* @return \Closure
*/
private function byDomain( $domain ) {
return function ( stdClass $entity ) use ( $domain ) {
return $entity->domain === $domain;
};
}
/**
* @param string $locale
*
* @return \Closure
*/
private function getFile( $locale ) {
return
function ( stdClass $entity ) use ( $locale ) {
return str_replace(
self::LOCALE_PLACEHOLDER,
$locale,
$entity->mofile_pattern
);
};
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace WPML\ST\MO\Notice;
class RegenerationInProgressNotice extends \WPML_Notice {
const ID = 'mo-files-regeneration';
const GROUP = 'mo-files';
public function __construct() {
$text = "WPML is updating the .mo files with the translation for strings. This will take a few more moments. During this process, translation for strings is not displaying on the front-end. You can refresh this page in a minute to see if it's done.";
$text = __( $text, 'wpml-string-translation' );
parent::__construct( self::ID, $text, self::GROUP );
$this->set_dismissible( false );
$this->set_css_classes( 'warning' );
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace WPML\ST\MO;
class Plural implements \IWPML_Backend_Action, \IWPML_Frontend_Action {
public function add_hooks() {
add_filter( 'ngettext', [ $this, 'handle_plural' ], 9, 5 );
add_filter( 'ngettext_with_context', [ $this, 'handle_plural_with_context' ], 9, 6 );
}
/**
* @param string $translation Translated text.
* @param string $single The text to be used if the number is singular.
* @param string $plural The text to be used if the number is plural.
* @param string $number The number to compare against to use either the singular or plural form.
* @param string $domain Text domain. Unique identifier for retrieving translated strings.
*
* @return string
*/
public function handle_plural( $translation, $single, $plural, $number, $domain ) {
return $this->get_translation( $translation, $single, $plural, $number, function ( $original ) use ( $domain ) {
return __( $original, $domain );
} );
}
/**
* @param string $translation Translated text.
* @param string $single The text to be used if the number is singular.
* @param string $plural The text to be used if the number is plural.
* @param string $number The number to compare against to use either the singular or plural form.
* @param string $context Context information for the translators.
* @param string $domain Text domain. Unique identifier for retrieving translated strings.
*
* @return string
*/
public function handle_plural_with_context( $translation, $single, $plural, $number, $context, $domain ) {
return $this->get_translation( $translation, $single, $plural, $number,
function ( $original ) use ( $domain, $context ) {
return _x( $original, $context, $domain );
} );
}
private function get_translation( $translation, $single, $plural, $number, $callback ) {
$original = (int) $number === 1 ? $single : $plural;
$possible_translation = $callback( $original );
if ( $possible_translation !== $original ) {
return $possible_translation;
}
return $translation;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace WPML\ST\MO;
use WP_Locale;
class WPLocaleProxy {
/**
* @var WP_Locale|null $wp_locale
*/
private $wp_locale;
/**
* @param string $method
* @param array $args
*
* @return mixed|null
*/
public function __call( $method, array $args ) {
if ( method_exists( $this->getWPLocale(), $method ) ) {
return call_user_func_array( [ $this->getWPLocale(), $method ], $args );
}
return null;
}
/**
* @param string $property
*
* @return bool
*/
public function __isset( $property ) {
if ( property_exists( \WP_Locale::class, $property ) ) {
return true;
}
return false;
}
/**
* @param string $property
*
* @return mixed|null
*/
public function __get( $property ) {
if ( $this->__isset( $property ) ) {
return $this->getWPLocale()->{$property};
}
return null;
}
/**
* @return WP_Locale|null
*/
private function getWPLocale() {
if ( ! $this->wp_locale ) {
$this->wp_locale = new WP_Locale();
}
return $this->wp_locale;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace WPML\ST;
class Shortcode {
const STRING_DOMAIN = 'wpml-shortcode';
private $context;
private $name;
/**
* @var \wpdb
*/
private $wpdb;
public function __construct( \wpdb $wpdb ) {
$this->wpdb = $wpdb;
}
function init_hooks() {
add_shortcode( 'wpml-string', [ $this, 'render' ] );
}
/**
* @param array $attributes
* @param string $value
*
* @return string
*/
function render( $attributes, $value ) {
$this->parse_attributes( $attributes, $value );
$this->maybe_register_string( $value );
return do_shortcode( icl_t( $this->context, $this->name, $value ) );
}
/**
* @param string $value
*/
private function maybe_register_string( $value ) {
$string = $this->get_registered_string();
if ( ! $string || $string->value !== $value ) {
icl_register_string( $this->context, $this->name, $value );
}
}
/**
* @param array $attributes
* @param string $value
*/
private function parse_attributes( $attributes, $value ) {
$pairs = array(
'context' => self::STRING_DOMAIN,
'name' => 'wpml-shortcode-' . md5( $value ),
);
$attributes = shortcode_atts( $pairs, $attributes );
$this->context = $attributes['context'];
$this->name = $attributes['name'];
}
/**
* @return \stdClass
*/
private function get_registered_string() {
$strings = $this->get_strings_registered_in_context();
if ( $strings && array_key_exists( $this->name, $strings ) ) {
return $strings[ $this->name ];
}
return null;
}
/**
* @return \stdClass[]
*/
private function get_strings_registered_in_context() {
$cache_key = $this->context;
$cache_group = 'wpml-string-shortcode';
$cache_found = false;
$string = wp_cache_get( $cache_key, $cache_group, false, $cache_found );
if ( ! $cache_found ) {
$query = 'SELECT name, id, value, status FROM ' . $this->wpdb->prefix . 'icl_strings WHERE context=%s';
$sql = $this->wpdb->prepare( $query, $this->context );
$string = $this->wpdb->get_results( $sql, OBJECT_K );
wp_cache_set( $cache_key, $string, $cache_group );
}
return $string;
}
}

View File

@@ -0,0 +1,216 @@
<?php
namespace WPML\ST;
use WPML\ST\Gettext\Settings as GettextSettings;
use WPML\ST\MO\Hooks\LanguageSwitch;
use WPML\ST\MO\File\Manager;
use WPML\ST\StringsFilter\Provider;
use WPML_Locale;
class TranslateWpmlString {
/** @var array $loadedDomains */
private static $loadedDomains = [];
/** @var Provider $filterProvider */
private $filterProvider;
/** @var LanguageSwitch $languageSwitch */
private $languageSwitch;
/** @var WPML_Locale $locale */
private $locale;
/** @var GettextSettings $gettextSettings */
private $gettextSettings;
/** @var Manager $fileManager */
private $fileManager;
/** @var bool $isAutoRegisterDisabled */
private $isAutoRegisterDisabled;
/** @var bool $lock */
private $lock = false;
public function __construct(
Provider $filterProvider,
LanguageSwitch $languageSwitch,
WPML_Locale $locale,
GettextSettings $gettextSettings,
Manager $fileManager
) {
$this->filterProvider = $filterProvider;
$this->languageSwitch = $languageSwitch;
$this->locale = $locale;
$this->gettextSettings = $gettextSettings;
$this->fileManager = $fileManager;
}
public function init() {
$this->languageSwitch->initCurrentLocale();
$this->isAutoRegisterDisabled = ! $this->gettextSettings->isAutoRegistrationEnabled();
}
/**
* @param string|array $wpmlContext
* @param string $name
* @param bool $value
* @param bool $allowEmptyValue
* @param null|bool $hasTranslation
* @param null|string $targetLang
*
* @return bool|string
*/
public function translate( $wpmlContext, $name, $value = false, $allowEmptyValue = false, &$hasTranslation = null, $targetLang = null ) {
if ( $this->lock ) {
return $value;
}
$this->lock = true;
if ( wpml_st_is_requested_blog() ) {
if ( $this->isAutoRegisterDisabled && self::canTranslateWithMO( $value, $name ) ) {
$value = $this->translateByMOFile( $wpmlContext, $name, $value, $hasTranslation, $targetLang );
} else {
$value = $this->translateByDBQuery( $wpmlContext, $name, $value, $hasTranslation, $targetLang );
}
}
$this->lock = false;
return $value;
}
/**
* @param string|array $wpmlContext
* @param string $name
* @param bool $value
* @param null|bool $hasTranslation
* @param null|string $targetLang
*
* @return string
*/
private function translateByMOFile( $wpmlContext, $name, $value, &$hasTranslation, $targetLang ) {
list ( $domain, $gettextContext ) = wpml_st_extract_context_parameters( $wpmlContext );
$translateByName = function ( $locale ) use ( $name, $domain, $gettextContext ) {
$this->loadTextDomain( $domain, $locale );
if ( $gettextContext ) {
return _x( $name, $gettextContext, $domain );
} else {
return __( $name, $domain );
}
};
$new_value = $this->withMOLocale( $targetLang, $translateByName );
$hasTranslation = $new_value !== $name;
if ( $hasTranslation ) {
$value = $new_value;
}
return $value;
}
/**
* @param string|array $wpmlContext
* @param string $name
* @param bool $value
* @param null|bool $hasTranslation
* @param null|string $targetLang
*
* @return string
*/
private function translateByDBQuery( $wpmlContext, $name, $value, &$hasTranslation, $targetLang ) {
$filter = $this->filterProvider->getFilter( $targetLang, $name );
if ( $filter ) {
$value = $filter->translate_by_name_and_context( $value, $name, $wpmlContext, $hasTranslation );
}
return $value;
}
/**
* @param string $domain
* @param string $locale
*/
private function loadTextDomain( $domain, $locale ) {
if (
! isset( $GLOBALS['l10n'][ $domain ] )
&& ! isset( $GLOBALS['l10n_unloaded'][ $domain ] )
&& ! isset( self::$loadedDomains[ $locale ][ $domain ] )
) {
load_textdomain(
$domain,
$this->fileManager->getFilepath( $domain, $locale )
);
self::$loadedDomains[ $locale ][ $domain ] = true;
}
}
/**
* @param string $targetLang
* @param callable $function
*
* @return string
*/
private function withMOLocale( $targetLang, $function ) {
$initialLocale = $this->languageSwitch->getCurrentLocale();
if ( $targetLang ) {
$targetLocale = $this->locale->get_locale( $targetLang );
$this->languageSwitch->switchToLocale( $targetLocale );
$result = $function( $targetLocale );
$this->languageSwitch->switchToLocale( $initialLocale );
} else {
$result = $function( $initialLocale );
}
return $result;
}
/**
* We will allow MO translation only when
* the original is not empty.
*
* We also need to make sure we deal with a
* WPML registered string (not gettext).
*
* If those conditions are not fulfilled,
* we will translate from the database.
*
* @param string $original
* @param string $name
*
* @return bool
*/
public static function canTranslateWithMO( $original, $name ) {
return $original && self::isWpmlRegisteredString( $original, $name );
}
/**
* This allows to differentiate WPML registered strings
* from gettext strings that have the default hash for
* the name.
*
* But it's still possible that WPML registered strings
* have a hash for the name.
*
* @param string $original
* @param string $name
*
* @return bool
*/
private static function isWpmlRegisteredString( $original, $name ) {
return $name && md5( $original ) !== $name;
}
public static function resetCache() {
self::$loadedDomains = [];
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace WPML\ST\Troubleshooting;
use WPML\ST\MO\Generate\MultiSite\Executor;
use WPML\ST\MO\Scan\UI\Factory;
use function WPML\Container\make;
use WPML\ST\MO\Generate\Process\Status;
use WPML\ST\Troubleshooting\Cleanup\Database;
class AjaxFactory implements \IWPML_AJAX_Action_Loader {
const ACTION_SHOW_GENERATE_DIALOG = 'wpml_st_mo_generate_show_dialog';
const ACTION_CLEANUP = 'wpml_st_troubleshooting_cleanup';
public function create() {
return self::getActions()->map( self::buildHandler() )->toArray();
}
/**
* @return \WPML\Collect\Support\Collection
*/
public static function getActions() {
return wpml_collect(
[
[ self::ACTION_SHOW_GENERATE_DIALOG, [ self::class, 'showGenerateDialog' ] ],
[ self::ACTION_CLEANUP, [ self::class, 'cleanup' ] ],
]
);
}
/**
* @return \Closure
*/
public static function buildHandler() {
return function( array $action ) {
return new RequestHandle( ...$action );
};
}
/**
* @throws \WPML\Auryn\InjectionException
*/
public static function showGenerateDialog() {
if ( is_super_admin() && is_multisite() ) {
( new Executor() )->executeWith(
Executor::MAIN_SITE_ID,
function () {
make( Status::class )->markIncompleteForAll();
}
);
} else {
make( Status::class )->markIncomplete();
}
Factory::ignoreWpmlVersion();
}
/**
* @throws \WPML\Auryn\InjectionException
*/
public static function cleanup() {
/** @var Database $database */
$database = make( Database::class );
$database->deleteStringsFromImportedMoFiles();
$database->truncatePagesAndUrls();
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace WPML\ST\Troubleshooting;
use function WPML\Container\make;
use WPML\ST\MO\Generate\DomainsAndLanguagesRepository;
use WPML\ST\MO\Generate\Process\ProcessFactory;
class BackendHooks implements \IWPML_Backend_Action, \IWPML_DIC_Action {
const SCRIPT_HANDLE = 'wpml-st-troubleshooting';
const NONCE_KEY = 'wpml-st-troubleshooting';
/** @var DomainsAndLanguagesRepository $domainsAndLanguagesRepo */
private $domainsAndLanguagesRepo;
public function __construct( DomainsAndLanguagesRepository $domainsAndLanguagesRepo ) {
$this->domainsAndLanguagesRepo = $domainsAndLanguagesRepo;
}
public function add_hooks() {
add_action( 'after_setup_complete_troubleshooting_functions', [ $this, 'displayButtons' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'loadJS' ] );
}
public function displayButtons() {
?><div>
<?php
if ( ! $this->domainsAndLanguagesRepo->get()->isEmpty() ) {
$this->displayButton(
AjaxFactory::ACTION_SHOW_GENERATE_DIALOG,
esc_attr__( 'Show custom MO Files Pre-generation dialog box', 'wpml-string-translation' ),
false
);
}
$this->displayButton(
AjaxFactory::ACTION_CLEANUP,
esc_attr__( 'Cleanup and optimize string tables', 'wpml-string-translation' ),
esc_attr__( 'Cleanup and optimization completed!', 'wpml-string-translation' )
);
?>
</div>
<?php
}
/**
* @param string $action
* @param string $buttonLabel
* @param string|false $confirmationMessage A string to display or false if we want to immediately reload.
*/
private function displayButton( $action, $buttonLabel, $confirmationMessage ) {
?>
<p>
<input id="<?php echo $action; ?>"
class="js-wpml-st-troubleshooting-action button-secondary"
type="button"
value="<?php echo $buttonLabel; ?>"
data-action="<?php echo $action; ?>"
data-success-message="<?php echo $confirmationMessage; ?>"
data-nonce="<?php echo wp_create_nonce( self::NONCE_KEY ); ?>"
data-reload="<?php echo ! (bool) $confirmationMessage; ?>"
/>
<br/>
</p>
<?php
}
/**
* @param string $hook
*/
public function loadJS( $hook ) {
if ( WPML_PLUGIN_FOLDER . '/menu/troubleshooting.php' === $hook ) {
wp_register_script(
self::SCRIPT_HANDLE,
WPML_ST_URL . '/res/js/troubleshooting.js',
[ 'jquery', 'wp-util', 'jquery-ui-sortable', 'jquery-ui-dialog' ]
);
wp_enqueue_script( self::SCRIPT_HANDLE );
}
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace WPML\ST\Troubleshooting\Cleanup;
use wpdb;
use WPML_ST_Translations_File_Dictionary;
class Database {
/** @var wpdb $wpdb */
private $wpdb;
/** @var WPML_ST_Translations_File_Dictionary $dictionary */
private $dictionary;
public function __construct(
wpdb $wpdb,
WPML_ST_Translations_File_Dictionary $dictionary
) {
$this->wpdb = $wpdb;
$this->dictionary = $dictionary;
}
public function deleteStringsFromImportedMoFiles() {
$moDomains = $this->dictionary->get_domains( 'mo' );
if ( ! $moDomains ) {
return;
}
$this->deleteOnlyNativeMoStringTranslations( $moDomains );
$this->deleteMoStringsWithNoTranslation( $moDomains );
icl_update_string_status_all();
$this->optimizeStringTables();
}
private function deleteOnlyNativeMoStringTranslations( array $moDomains ) {
$this->wpdb->query(
"
DELETE st FROM {$this->wpdb->prefix}icl_string_translations AS st
LEFT JOIN {$this->wpdb->prefix}icl_strings AS s
ON st.string_id = s.id
WHERE st.value IS NULL AND s.context IN(" . wpml_prepare_in( $moDomains ) . ')
'
);
}
private function deleteMoStringsWithNoTranslation( array $moDomains ) {
$this->wpdb->query(
"
DELETE s FROM {$this->wpdb->prefix}icl_strings AS s
LEFT JOIN {$this->wpdb->prefix}icl_string_translations AS st
ON st.string_id = s.id
WHERE st.string_id IS NULL AND s.context IN(" . wpml_prepare_in( $moDomains ) . ')
'
);
}
private function optimizeStringTables() {
$this->wpdb->query( "OPTIMIZE TABLE {$this->wpdb->prefix}icl_strings, {$this->wpdb->prefix}icl_string_translations" );
}
public function truncatePagesAndUrls() {
foreach ( [ 'icl_string_pages', 'icl_string_urls' ] as $table ) {
$table = $this->wpdb->prefix . $table;
if ( $this->tableExists( $table ) ) {
$this->wpdb->query( "TRUNCATE $table" );
}
}
}
/**
* @param string $table
*
* @return bool
*/
private function tableExists( $table ) {
return (bool) $this->wpdb->get_var( $this->wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace WPML\ST\Troubleshooting;
class RequestHandle implements \IWPML_Action {
/** @var string $action */
private $action;
/** @var callable $callback */
private $callback;
public function __construct( $action, $callback ) {
$this->action = $action;
$this->callback = $callback;
}
public function add_hooks() {
add_action( 'wp_ajax_' . $this->action, [ $this, 'handle' ] );
}
public function handle() {
if ( wp_verify_nonce( $_POST['nonce'], BackendHooks::NONCE_KEY ) ) {
call_user_func( $this->callback );
wp_send_json_success();
} else {
wp_send_json_error( 'Invalid nonce value', 500 );
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace WPML\ST;
class Actions {
public static function get() {
return array(
'WPML_ST_Theme_Plugin_Localization_Resources_Factory',
'WPML_ST_Theme_Plugin_Localization_Options_UI_Factory',
'WPML_ST_Theme_Plugin_Localization_Options_Settings_Factory',
'WPML_ST_Theme_Plugin_Scan_Dir_Ajax_Factory',
'WPML_ST_Theme_Plugin_Scan_Files_Ajax_Factory',
'WPML_ST_Update_File_Hash_Ajax_Factory',
'WPML_ST_Theme_Plugin_Hooks_Factory',
'WPML_ST_Taxonomy_Labels_Translation_Factory',
'WPML_ST_String_Translation_AJAX_Hooks_Factory',
'WPML_ST_Remote_String_Translation_Factory',
'WPML_ST_Privacy_Content_Factory',
'WPML_ST_String_Tracking_AJAX_Factory',
\WPML_ST_Translation_Memory::class,
'WPML_ST_Script_Translations_Hooks_Factory',
\WPML\ST\MO\Scan\UI\Factory::class,
'WPML\ST\Rest\FactoryLoader',
\WPML\ST\Gettext\HooksFactory::class,
\WPML_ST_Support_Info_Filter::class,
\WPML\ST\Troubleshooting\BackendHooks::class,
\WPML\ST\Troubleshooting\AjaxFactory::class,
\WPML\ST\MO\File\FailureHooksFactory::class,
\WPML\ST\DB\Mappers\Hooks::class,
\WPML\ST\Shortcode\Hooks::class,
\WPML\ST\AdminTexts\UI::class,
\WPML\ST\PackageTranslation\Hooks::class,
\WPML\ST\Main\UI::class,
\WPML\ST\StringsCleanup\UI::class,
\WPML\ST\DisplayAsTranslated\CheckRedirect::class,
);
}
}

View File

@@ -0,0 +1,26 @@
<?php
class WPML_ST_Remote_String_Translation_Factory implements IWPML_Backend_Action_Loader, IWPML_Action {
public function create() {
return $this;
}
public function add_hooks() {
if ( did_action( 'wpml_tm_loaded' ) ) {
$this->on_tm_loaded();
} else {
add_action( 'wpml_tm_loaded', array( $this, 'on_tm_loaded' ) );
}
}
public function on_tm_loaded() {
if ( ! class_exists( 'WPML_Manage_Translations_Role' ) ) {
return;
}
if ( current_user_can( WPML_Manage_Translations_Role::CAPABILITY ) ) {
add_action( 'wpml_st_below_menu', array( 'WPML_Remote_String_Translation', 'display_string_menu' ) );
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
class WPML_ST_WP_Loaded_Action extends WPML_SP_User {
/** @var WPML_String_Translation $st_instance */
private $st_instance;
/** @var string $pagenow */
private $pagenow;
/** @var string $get_page */
private $get_page;
public function __construct( &$sitepress, &$st_instance, &$pagenow, $get_page ) {
parent::__construct( $sitepress );
$this->st_instance = &$st_instance;
$this->pagenow = &$pagenow;
$this->get_page = $get_page;
}
public function run() {
$string_settings = $this->sitepress->get_setting( 'st', array() );
if ( ! isset( $string_settings['sw'] )
|| ( $this->pagenow === 'admin.php'
&& strpos( $this->get_page, 'theme-localization.php' ) !== false ) ) {
$string_settings['sw'] = isset( $string_settings['sw'] )
? $string_settings['sw'] : array();
$this->sitepress->set_setting( 'st', $string_settings, true );
$this->st_instance->initialize_wp_and_widget_strings();
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
class WPML_ST_Admin_Blog_Option extends WPML_SP_User {
/** @var WPML_ST_Admin_Option_Translation $admin_option */
private $admin_option;
/**
* WPML_ST_Admin_Blog_Option constructor.
*
* @param SitePress $sitepress
* @param WPML_String_Translation $st_instance
* @param string $option_name
*/
public function __construct(
&$sitepress,
&$st_instance,
$option_name
) {
if ( ! WPML_ST_Blog_Name_And_Description_Hooks::is_string( $option_name ) ) {
throw new InvalidArgumentException( $option_name . ' Is not a valid blog option that is handled by this class, allowed values are "Tagline" and "Blog Title"' );
}
parent::__construct( $sitepress );
$this->admin_option = $st_instance->get_admin_option( $option_name );
}
/**
* @param string $old_value
* @param string $new_value
*
* @return mixed
*/
public function pre_update_filter(
$old_value,
$new_value
) {
$wp_api = $this->sitepress->get_wp_api();
if ( $wp_api->is_multisite() && $wp_api->ms_is_switched() && ! $this->sitepress->get_setting( 'setup_complete' ) ) {
throw new RuntimeException( 'You cannot update blog option translations while switched to a blog on which the WPML setup is not complete! You are currently using blog ID:' . $this->sitepress->get_wp_api()->get_current_blog_id() );
}
WPML_Config::load_config_run();
return $this->admin_option->update_option(
'',
$new_value,
ICL_TM_COMPLETE
) ? $old_value : $new_value;
}
}

View File

@@ -0,0 +1,93 @@
<?php
class WPML_ST_Admin_Option_Translation extends WPML_SP_User {
/** @var WPML_String_Translation $st_instance */
private $st_instance;
/** @var string $option_name */
private $option_name;
/** @var string $option_name */
private $language;
/**
* WPML_ST_Admin_Option constructor.
*
* @param SitePress $sitepress
* @param WPML_String_Translation $st_instance
* @param string $option_name
* @param string $language
*/
public function __construct(
&$sitepress,
&$st_instance,
$option_name,
$language = ''
) {
if ( ! $option_name || ! is_scalar( $option_name ) ) {
throw new InvalidArgumentException( 'Not a valid option name, received: ' . serialize( $option_name ) );
}
parent::__construct( $sitepress );
$this->st_instance = &$st_instance;
$this->option_name = $option_name;
$this->language = $language ? $language : $this->st_instance->get_current_string_language( $option_name );
}
/**
*
* @param string $option_name
* @param string $new_value
* @param int|bool $status
* @param int $translator_id
* @param int $rec_level
*
* @return boolean|mixed
*/
public function update_option(
$option_name = '',
$new_value = null,
$status = false,
$translator_id = null,
$rec_level = 0
) {
$option_name = $option_name ? $option_name : $this->option_name;
$new_value = (array) $new_value;
$updated = array();
foreach ( $new_value as $index => $value ) {
if ( is_array( $value ) ) {
$name = '[' . $option_name . '][' . $index . ']';
$result = $this->update_option(
$name,
$value,
$status,
$translator_id,
$rec_level + 1
);
$updated[] = array_sum( explode( ',', $result ) );
} else {
if ( is_string( $index ) ) {
$name = ( $rec_level == 0 ? '[' . $option_name . ']' : $option_name ) . $index;
} else {
$name = $option_name;
}
$string = $this->st_instance->string_factory()->find_admin_by_name( $name );
$string_id = $string->string_id();
if ( $string_id ) {
if ( $this->language !== $string->get_language() ) {
$updated[] = $string->set_translation(
$this->language,
$value,
$status,
$translator_id
);
} else {
$string->update_value( $value );
}
}
}
}
return array_sum( $updated ) > 0 ? join( ',', $updated ) : false;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace WPML\ST\Basket;
use WPML\FP\Obj;
class Status {
public static function add( array $translations, $languages ) {
$statusProvider = [ 'TranslationProxy_Basket', 'is_in_basket' ];
if ( is_callable( $statusProvider ) ) {
$translations = self::addWithProvider( $translations, $languages, $statusProvider );
}
return $translations;
}
private static function addWithProvider( array $translations, $languages, callable $statusProvider ) {
foreach ( $translations as $id => $string ) {
foreach ( Obj::propOr( [], 'translations', $string ) as $lang => $data ) {
$translations[ $id ]['translations'][ $lang ]['in_basket'] = $statusProvider( $id, $string['string_language'], $lang, 'string' );
}
foreach ( $languages as $lang ) {
if (
$lang !== $string['string_language']
&& ! isset( $translations[ $id ]['translations'][ $lang ] )
&& $statusProvider( $id, $string['string_language'], $lang, 'string' )
) {
$translations[ $id ]['translations'][ $lang ] = [
'id' => 0,
'language' => $lang,
'in_basket' => true,
];
}
}
}
return $translations;
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace WPML\ST\Batch\Translation;
use WPML\Collect\Support\Traits\Macroable;
use WPML\FP\Fns;
use WPML\FP\Relation;
use function WPML\FP\curryN;
use function WPML\FP\invoke;
use function WPML\FP\pipe;
/**
* Class Convert
*
* @package WPML\ST\Batch\Translation
* @method static callable|array toBatchElements( ...$getBatchId, ...$setBatchRecord, ...$elements, ...$basketName ) :: ( string → int ) → ( int → int → string ) → [WPML_TM_Translation_Batch_Element] → string → [WPML_TM_Translation_Batch_Element]
*/
class Convert {
use Macroable;
public static function init() {
self::macro(
'toBatchElements',
curryN(
4,
function ( $getBatchId, $setBatchRecord, $elements, $basketName ) {
// $isString :: WPML_TM_Translation_Batch_Element → bool
$isString = pipe( invoke( 'get_element_type' ), Relation::equals( 'string' ) );
list( $stringElements, $otherElements ) = wpml_collect( $elements )->partition( $isString );
$makeBatchPerLanguage = function ( \WPML_TM_Translation_Batch_Element $element ) use ( $getBatchId, $setBatchRecord, $basketName ) {
$makeBatchElement = function ( $action, $lang ) use ( $element, $getBatchId, $setBatchRecord, $basketName ) {
$batchId = $getBatchId( $basketName . '-' . $lang );
$setBatchRecord( $batchId, $element->get_element_id(), $element->get_source_lang() );
return Fns::makeN(
5,
'WPML_TM_Translation_Batch_Element',
$batchId,
'st-batch',
$element->get_source_lang(),
[ $lang => $action ],
[]
);
};
return Fns::map( $makeBatchElement, $element->get_target_langs() );
};
$stringElements = $stringElements->map( $makeBatchPerLanguage )
->flatten()
->unique( invoke( 'get_target_langs' ) );
return $otherElements->merge( $stringElements )
->toArray();
}
)
);
}
}
Convert::init();

View File

@@ -0,0 +1,67 @@
<?php
namespace WPML\ST\Batch\Translation;
use WPML\FP\Fns;
use WPML\FP\Obj;
use WPML\LIB\WP\Hooks as WPHooks;
use function WPML\FP\spreadArgs;
class Hooks {
public static function addHooks(
callable $getBatchId,
callable $setBatchRecord,
callable $getBatchRecord,
callable $getString
) {
WPHooks::onFilter( 'wpml_tm_batch_factory_elements', 10, 2 )
->then( spreadArgs( Convert::toBatchElements( $getBatchId, $setBatchRecord ) ) );
WPHooks::onFilter( 'wpml_tm_basket_items_types', 10, 1 )
->then( spreadArgs( Obj::set( Obj::lensProp( 'st-batch' ), 'core' ) ) );
WPHooks::onFilter( 'wpml_is_external', 10, 2 )
->then(
spreadArgs(
function ( $state, $type ) {
return $state
|| ( is_object( $type ) && Obj::prop( 'post_type', $type ) === 'strings' )
|| $type === 'st-batch';
}
)
);
WPHooks::onFilter( 'wpml_get_translatable_item', 10, 3 )
->then( spreadArgs( Strings::get( $getBatchRecord, $getString ) ) );
WPHooks::onAction( 'wpml_save_external', 10, 3 )
->then( spreadArgs( StringTranslations::save() ) );
WPHooks::onFilter( 'wpml_tm_populate_prev_translation', 10, 3 )
->then( spreadArgs( StringTranslations::addExisting() ) );
}
public static function addStringTranslationStatusHooks(
callable $updateTranslationStatus,
callable $initializeTranslation
) {
WPHooks::onAction( 'wpml_tm_added_translation_element', 10, 2 )->then( spreadArgs( $initializeTranslation ) );
WPHooks::onAction( 'wpml_tm_job_in_progress', 10, 2 )->then( spreadArgs( $updateTranslationStatus ) );
WPHooks::onAction( 'wpml_tm_job_cancelled', 10, 1 )->then( spreadArgs( StringTranslations::cancelTranslations() ) );
WPHooks::onAction( 'wpml_tm_jobs_cancelled', 10, 1 )->then( spreadArgs( function ( $jobs ) {
/**
* We need this check because if we pass only one job to the hook:
* do_action( 'wpml_tm_jobs_cancelled', [ $job ] )
* then WordPress converts it to $job.
*/
if ( is_object( $jobs ) ) {
$jobs = [ $jobs ];
}
Fns::map( StringTranslations::cancelTranslations(), $jobs );
} ) );
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace WPML\ST\Batch\Translation;
use WPML\Collect\Support\Traits\Macroable;
use WPML\FP\Fns;
use function WPML\Container\make;
use function WPML\FP\curryN;
use function WPML\FP\partial;
/**
* Class Module
* @package WPML\ST\Batch\Translation
* @method static callable|string getString( ...$id ) :: int → string
* @method static callable getBatchId() :: ( string → int )
* @method static callable|void batchStringsStorage( ...$records, ...$batchId, ...$stringId, ...$sourceLang ) :: Records → int → int → string → void
* @method static callable|void setBatchLanguage( ...$batchId, ...$sourceLang ) :: int → string → void
*/
class Module {
use Macroable;
const EXTERNAL_TYPE = 'st-batch_strings';
const STRING_ID_PREFIX = 'batch-string-';
public static function init() {
global $sitepress, $wpdb;
Records::installSchema( $wpdb );
self::macro( 'getString', curryN( 1, function ( $id ) {
return make( '\WPML_ST_String', [ ':string_id' => $id ] )->get_value();
} ) );
self::macro( 'getBatchId', curryN( 1, function ( $batch ) {
return \TranslationProxy_Batch::update_translation_batch( $batch );
} ) );
$setLanguage = curryN( 4, [ $sitepress, 'set_element_language_details' ] );
self::macro( 'setBatchLanguage', $setLanguage( Fns::__, self::EXTERNAL_TYPE, null, Fns::__ ) );
self::macro( 'batchStringsStorage', curryN( 4, function ( callable $saveBatch, $batchId, $stringId, $sourceLang ) {
self::setBatchLanguage( $batchId, $sourceLang );
$saveBatch( $batchId, $stringId );
} ) );
$initializeTranslation = StringTranslations::markTranslationsAsInProgress(
partial( [ Status::class, 'getStatusesOfBatch' ], $wpdb )
);
Hooks::addHooks(
self::getBatchId(),
self::batchStringsStorage( Records::set( $wpdb ) ),
Records::get( $wpdb ),
self::getString()
);
Hooks::addStringTranslationStatusHooks( StringTranslations::updateStatus(), $initializeTranslation );
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace WPML\ST\Batch\Translation;
use WPML\FP\Curryable;
use WPML\FP\Fns;
use WPML\FP\Lst;
use function WPML\Container\make;
/**
* @method static callable|void installSchema( ...$wpdb ) :: wpdb → void
* @method static callable|int get( ...$wpdb, ...$batchId ) :: wpdb → int → [int]
* @method static callable|void set( ...$wpdb, ...$batchId, ...$stringId ) :: wpdb → int → int → void
* @method static callable|int[] findBatches( ...$wpdb, ...$stringId ) :: wpdb → int → int[]
*/
class Records {
use Curryable;
/** @var string */
public static $string_batch_sql_prototype = '
CREATE TABLE IF NOT EXISTS `%sicl_string_batches` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`string_id` bigint(20) unsigned NOT NULL,
`batch_id` bigint(20) unsigned NOT NULL,
PRIMARY KEY (`id`)
)
';
}
Records::curryN(
'installSchema',
1,
function ( \wpdb $wpdb ) {
$option = make( 'WPML\WP\OptionManager' );
if ( ! $option->get( 'ST', Records::class . '_schema_installed' ) ) {
$wpdb->query( sprintf( Records::$string_batch_sql_prototype, $wpdb->prefix ) );
$option->set( 'ST', Records::class . '_schema_installed', true );
}
}
);
Records::curryN(
'get',
2,
function ( \wpdb $wpdb, $batchId ) {
return $wpdb->get_col(
$wpdb->prepare( "SELECT string_id FROM {$wpdb->prefix}icl_string_batches WHERE batch_id = %d", $batchId )
);
}
);
Records::curryN(
'set',
3,
function ( \wpdb $wpdb, $batchId, $stringId ) {
// TODO: ignore duplicates
$wpdb->insert(
"{$wpdb->prefix}icl_string_batches",
[
'batch_id' => $batchId,
'string_id' => $stringId,
],
[ '%d', '%d' ]
);
}
);
Records::curryN(
'findBatch',
2,
function ( \wpdb $wpdb, $stringId ) {
return $wpdb->get_var(
$wpdb->prepare( "SELECT batch_id FROM {$wpdb->prefix}icl_string_batches WHERE string_id = %d", $stringId )
);
}
);
Records::curryN(
'findBatches',
2,
function ( \wpdb $wpdb, $stringIds ) {
$in = wpml_prepare_in( $stringIds, '%d' );
$data = $wpdb->get_results(
"SELECT batch_id, string_id FROM {$wpdb->prefix}icl_string_batches WHERE string_id IN ({$in})"
);
$keyByStringId = Fns::converge( Lst::zipObj(), [ Lst::pluck( 'string_id' ), Lst::pluck( 'batch_id' ) ] );
return $keyByStringId( $data );
}
);

View File

@@ -0,0 +1,83 @@
<?php
namespace WPML\ST\Batch\Translation;
use WPML\FP\Lst;
use WPML\FP\Fns;
use WPML\FP\Obj;
class Status {
public static function add( array $translations, $languages ) {
global $wpdb;
$batches = Records::findBatches( $wpdb, array_keys( $translations ) );
$statuses = self::getStatuses( $wpdb, $batches );
foreach ( $translations as $id => $string ) {
foreach ( Obj::propOr( [], 'translations', $string ) as $lang => $data ) {
$status = Obj::pathOr( null, [ $id, $lang ], $statuses );
if ( $status ) {
$translations[ $id ]['translations'][ $lang ]['status'] = $status;
}
}
}
return $translations;
}
public static function getStatuses( \wpdb $wpdb, $batches ) {
$batchIds = array_unique( array_values( $batches ) );
if ( $batchIds ) {
$in = wpml_prepare_in( $batchIds, '%d' );
$trids = $wpdb->get_results(
"SELECT element_id, trid FROM {$wpdb->prefix}icl_translations WHERE element_id IN ({$in}) AND element_type = 'st-batch_strings'"
);
$keyByBatchId = Fns::converge( Lst::zipObj(), [ Lst::pluck( 'element_id' ), Lst::pluck( 'trid' ) ] );
$trids = $keyByBatchId( $trids );
$in = wpml_prepare_in( $trids, '%d' );
$transIds = $wpdb->get_results(
"SELECT translation_id, trid, language_code FROM {$wpdb->prefix}icl_translations WHERE trid IN ({$in}) AND source_language_code IS NOT NULL"
);
$in = wpml_prepare_in( Lst::pluck( 'translation_id', $transIds ), '%d' );
$statuses = $wpdb->get_results(
"SELECT status, translation_id FROM {$wpdb->prefix}icl_translation_status WHERE translation_id IN ({$in})"
);
$keyByTranslationId = Fns::converge(
Lst::zipObj(),
[
Lst::pluck( 'translation_id' ),
Lst::pluck( 'status' ),
]
);
$statuses = $keyByTranslationId( $statuses );
$keyByTrid = Fns::converge( Lst::zipObj(), [ Lst::pluck( 'trid' ), Fns::identity() ] );
return wpml_collect( $batches )
->map( Obj::prop( Fns::__, $trids ) )
->map( Obj::prop( Fns::__, $keyByTrid( $transIds ) ) )
->map(
function ( $item ) use ( $statuses ) {
return [ $item->language_code => Obj::prop( $item->translation_id, $statuses ) ];
}
)
->toArray();
} else {
return [];
}
}
public static function getStatusesOfBatch( \wpdb $wpdb, $batchId ) {
$statuses = self::getStatuses( $wpdb, [ $batchId ] );
return count( $statuses ) ? current( $statuses ) : [];
}
}

View File

@@ -0,0 +1,216 @@
<?php
namespace WPML\ST\Batch\Translation;
use WPML\Collect\Support\Traits\Macroable;
use WPML\FP\Fns;
use WPML\FP\Logic;
use WPML\FP\Obj;
use WPML\FP\Str;
use WPML\FP\Wrapper;
use WPML\Setup\Option;
use WPML\ST\API\Fns as ST_API;
use function WPML\Container\make;
use function WPML\FP\curryN;
use function WPML\FP\invoke;
use function WPML\FP\pipe;
use function WPML\FP\spreadArgs;
/**
* Class StringTranslations
*
* @package WPML\ST\Batch\Translation
* @method static callable|void save( ...$element_type_prefix, ...$job, ...$decoder ) :: string → object → ( string → string → string ) → void
* @method static callable|void addExisting( ...$prevTranslations, ...$package, ...$lang ) :: [WPML_TM_Translated_Field] → object → string → [WPML_TM_Translated_Field]
* @method static callable|bool isTranslated( ...$field ) :: object → bool
* @method static callable|bool isBatchId( ...$str ) :: string → bool
* @method static callable|bool isBatchField( ...$field ) :: object → bool
* @method static callable|string decodeStringId( ...$str ) :: string → string
* @method static callable|void markTranslationsAsInProgress( ...$getJobStatus, ...$hasTranslation, ...$addTranslation, ...$post, ...$element) :: callable -> callable -> callable -> WPML_TM_Translation_Batch_Element -> \stdClass -> void
* @method static callable|void cancelTranslations(...$job) :: \WPML_TM_Job_Entity -> void
* @method static callable|void updateStatus(...$element_type_prefix, ...$job) :: string -> \stdClass -> void
*/
class StringTranslations {
use Macroable;
public static function init() {
self::macro( 'isBatchId', Str::startsWith( Module::STRING_ID_PREFIX ) );
self::macro( 'isTranslated', Obj::prop( 'field_translate' ) );
self::macro(
'isBatchField',
curryN(
1,
function( $field ) {
return self::isBatchId( Obj::prop( 'field_type', $field ) );
}
)
);
self::macro( 'decodeStringId', Str::replace( Module::STRING_ID_PREFIX, '' ) );
self::macro(
'save',
curryN(
3,
function ( $element_type_prefix, $job, callable $decoder ) {
if ( $element_type_prefix === 'st-batch' ) {
// $decodeField :: field → string
$decodeField = pipe(
Obj::props( [ 'field_data_translated', 'field_format' ] ),
spreadArgs( $decoder )
);
// $getStringId :: field → int
$getStringId = pipe( Obj::prop( 'field_type' ), self::decodeStringId() );
// $saveTranslation :: field → void
$saveTranslation = Fns::converge(
ST_API::saveTranslation( Fns::__, $job->language_code, Fns::__, ICL_TM_COMPLETE ),
[ $getStringId, $decodeField ]
);
Wrapper::of( $job->elements )
->map( Fns::filter( Logic::allPass( [ self::isTranslated(), self::isBatchField() ] ) ) )
->map( Fns::each( $saveTranslation ) );
}
}
)
);
self::macro(
'updateStatus',
curryN(
2,
function ( $element_type_prefix, $job ) {
if ( $element_type_prefix === 'st-batch' ) {
// $getStringId :: field → int
$getStringId = pipe( Obj::prop( 'field_type' ), self::decodeStringId() );
$updateStatus = ST_API::updateStatus( Fns::__, $job->language_code, ICL_TM_IN_PROGRESS );
\wpml_collect( $job->elements )
->filter( self::isBatchField() )
->map( $getStringId )
->each( $updateStatus );
}
}
)
);
self::macro(
'cancelTranslations',
curryN(
1,
function ( $job ) {
if ( $job instanceof \WPML_TM_Post_Job_Entity && $job->get_type() === 'st-batch_strings' ) {
$language = $job->get_target_language();
// $getTranslations :: $stringId -> [stringId, translation]
$getTranslations = function ( $stringId ) use ( $language ) {
return [
'string_id' => $stringId,
'translation' => Obj::pathOr( '', [ $language, 'value' ], ST_API::getTranslations( $stringId ) ),
];
};
// $cancelStatus :: [stringId, translation] -> int
$cancelStatus = Logic::ifElse( Obj::prop( 'translation' ), Fns::always( ICL_TM_COMPLETE ), Fns::always( ICL_TM_NOT_TRANSLATED ) );
// $cancel :: [stringId, translation] -> void
$cancel = Fns::converge(
ST_API::updateStatus( Fns::__, $language, Fns::__ ),
[ Obj::prop( 'string_id' ), $cancelStatus ]
);
\wpml_collect( $job->get_elements() )
->map( invoke( 'get_type' ) )
->filter( Fns::unary( self::isBatchId() ) )
->map( self::decodeStringId() )
->map( $getTranslations )
->map( Fns::tap( $cancel ) );
}
}
)
);
self::macro(
'addExisting',
curryN(
3,
function ( $prevTranslations, $package, $lang ) {
// $getTranslation :: lang → { translate, ... } → int → { id, translation } | null
$getTranslation = curryN(
3,
function ( $lang, $data, $stringId ) {
if ( $data['translate'] === 1 && self::isBatchId( $stringId ) ) {
return (object) [
'id' => $stringId,
'translation' => base64_encode( ST_API::getTranslation( self::decodeStringId( $stringId ), $lang ) ),
];
}
return null;
}
);
// $createField :: string → WPML_TM_Translated_Field
$createField = function ( $translation ) {
return make( 'WPML_TM_Translated_Field', [ '', '', $translation, false ] );
};
// $updatePrevious :: [a] → { id, translate } → [a]
$updatePrevious = function ( $prev, $string ) {
$prev[ $string->id ] = $string->translation;
return $prev;
};
// $hasTranslation :: { id, translation } | null → bool
$hasTranslation = Obj::prop( 'translation' );
return Wrapper::of( $package['contents'] )
->map( Fns::map( $getTranslation( $lang ) ) )
->map( Fns::filter( $hasTranslation ) )
->map( Fns::map( Obj::evolve( [ 'translation' => $createField ] ) ) )
->map( Fns::reduce( $updatePrevious, $prevTranslations ) )
->get();
}
)
);
self::macro(
'markTranslationsAsInProgress',
curryN(
3,
function ( $getJobStatus, $element, $post ) {
if ( $element instanceof \WPML_TM_Translation_Batch_Element && $element->get_element_type() === 'st-batch' ) {
$statuses = \wpml_collect( $getJobStatus( $post->post_id ) );
$addTranslationWithStatus = function ( $stringId, $targetLanguage ) use ( $statuses ) {
$status = Option::shouldTranslateEverything()
? ICL_TM_IN_PROGRESS
: $statuses->get( $targetLanguage, ICL_STRING_TRANSLATION_NOT_TRANSLATED );
ST_API::updateStatus( $stringId, $targetLanguage, $status );
};
\wpml_collect( $post->string_data )
->keys()
->map( Fns::unary( StringTranslations::decodeStringId() ) )
->map( Fns::unary( 'intval' ) )
->crossJoin( array_keys( $element->get_target_langs() ) )
->map( Fns::tap( spreadArgs( $addTranslationWithStatus ) ) );
}
}
)
);
}
}
StringTranslations::init();

View File

@@ -0,0 +1,51 @@
<?php
namespace WPML\ST\Batch\Translation;
use WPML\Collect\Support\Traits\Macroable;
use WPML\FP\Fns;
use function WPML\FP\curryN;
/**
* Class Strings
*
* @package WPML\ST\Batch\Translation
* @method static callable|object get( ...$getBatchRecord, ...$getString, ...$item, ...$id, ...$type )
*/
class Strings {
use Macroable;
public static function init() {
self::macro(
'get',
curryN(
5,
function ( callable $getBatchRecord, callable $getString, $item, $id, $type ) {
if ( $type === 'st-batch' || $type === Module::EXTERNAL_TYPE ) {
$getBatchString = function ( $strings, $stringId ) use ( $getString ) {
$strings[ Module::STRING_ID_PREFIX . $stringId ] = $getString( $stringId );
return $strings;
};
return (object) [
'post_id' => $id,
'ID' => $id,
'post_type' => 'strings',
'kind' => 'Strings',
'kind_slug' => 'Strings',
'external_type' => true,
'string_data' => Fns::reduce( $getBatchString, [], $getBatchRecord( $id ) ),
];
}
return $item;
}
)
);
}
}
Strings::init();

View File

@@ -0,0 +1,9 @@
<?php
class WPML_Admin_Notifier {
public function display_instant_message( $message, $type = 'information', $class = false, $return = false, $fadeout = false ) {
return ICL_AdminNotifier::display_instant_message( $message, $type, $class, $return, $fadeout );
}
}

View File

@@ -0,0 +1,44 @@
<?php
class WPML_Language_Of_Domain {
/**
* @var SitePress
*/
private $sitepress;
/**
* @var array
*/
private $language_of_domain = array();
/**
* @param SitePress $sitepress
*/
public function __construct( SitePress $sitepress ) {
$this->sitepress = $sitepress;
$string_settings = $this->sitepress->get_setting( 'st' );
if ( isset( $string_settings['lang_of_domain'] ) ) {
$this->language_of_domain = $string_settings['lang_of_domain'];
}
}
public function get_language( $domain ) {
$lang = null;
if ( isset( $this->language_of_domain[ $domain ] ) ) {
$lang = $this->language_of_domain[ $domain ];
}
return $lang;
}
public function set_language( $domain, $lang ) {
$this->language_of_domain[ $domain ] = $lang;
$string_settings = $this->sitepress->get_setting( 'st' );
$string_settings['lang_of_domain'] = $this->language_of_domain;
$this->sitepress->set_setting( 'st', $string_settings, true );
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* WPML_ST_Admin_String class
*/
class WPML_ST_Admin_String extends WPML_ST_String {
/**
* @var string $name
*/
private $name;
/**
* @var string $value
*/
private $value;
/**
* @param string $new_value
*/
public function update_value( $new_value ) {
$this->fetch_name_and_value();
if ( md5( $this->value ) !== $this->name ) {
$this->value = $new_value;
$this->set_property( 'value', $new_value );
$this->update_status();
}
}
private function fetch_name_and_value() {
if ( is_null( $this->name ) || is_null( $this->value ) ) {
$res = $this->wpdb->get_row(
'SELECT name, value ' . $this->from_where_snippet()
);
$this->name = $res->name;
$this->value = $res->value;
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
class WPML_ST_Initialize {
public function load() {
add_action( 'plugins_loaded', array( $this, 'run' ), - PHP_INT_MAX );
}
public function run() {
if ( ! $this->hasMinimalCoreRequirements() ) {
return;
}
$this->includeAutoloader();
$this->configureDIC();
$this->loadEarlyHooks();
}
private function hasMinimalCoreRequirements() {
if ( ! class_exists( 'WPML_Core_Version_Check' ) ) {
require_once WPML_ST_PATH . '/vendor/wpml-shared/wpml-lib-dependencies/src/dependencies/class-wpml-core-version-check.php';
}
return WPML_Core_Version_Check::is_ok( WPML_ST_PATH . '/wpml-dependencies.json' );
}
private function includeAutoloader() {
require_once WPML_ST_PATH . '/vendor/autoload.php';
}
private function configureDIC() {
\WPML\Container\share( \WPML\ST\Container\Config::getSharedClasses() );
\WPML\Container\alias( \WPML\ST\Container\Config::getAliases() );
\WPML\Container\delegate( \WPML\ST\Container\Config::getDelegated() );
}
private function loadEarlyHooks() {
/** @var \WPML\ST\TranslationFile\Hooks $hooks */
$hooks = \WPML\Container\make( \WPML\ST\TranslationFile\Hooks::class );
$hooks->install();
}
}

View File

@@ -0,0 +1,65 @@
<?php
class WPML_ST_Reset {
/**
* @var wpdb
*/
private $wpdb;
/**
* @var WPML_ST_Settings
*/
private $settings;
/**
* @param wpdb $wpdb
* @param WPML_ST_Settings $settings
*/
public function __construct( $wpdb, WPML_ST_Settings $settings = null ) {
$this->wpdb = $wpdb;
if ( ! $settings ) {
$settings = new WPML_ST_Settings();
}
$this->settings = $settings;
}
public function reset() {
$this->settings->delete_settings();
// remove tables at the end to avoid errors in ST due to last actions invoked by hooks
add_action( 'shutdown', array( $this, 'remove_db_tables' ), PHP_INT_MAX - 1 );
}
public function remove_db_tables() {
$blog_id = $this->retrieve_current_blog_id();
$is_multisite_reset = $blog_id && function_exists( 'is_multisite' ) && is_multisite();
if ( $is_multisite_reset ) {
switch_to_blog( $blog_id );
}
$table = $this->wpdb->prefix . 'icl_string_pages';
$this->wpdb->query( 'DROP TABLE IF EXISTS ' . $table );
$table = $this->wpdb->prefix . 'icl_string_urls';
$this->wpdb->query( 'DROP TABLE IF EXISTS ' . $table );
if ( $is_multisite_reset ) {
restore_current_blog();
}
}
/**
* @return int
*/
private function retrieve_current_blog_id() {
$filtered_id = array_key_exists( 'id', $_POST )
? filter_var( $_POST['id'], FILTER_SANITIZE_NUMBER_INT ) : false;
$filtered_id = array_key_exists( 'id', $_GET ) && ! $filtered_id ?
filter_var( $_GET['id'], FILTER_SANITIZE_NUMBER_INT ) : $filtered_id;
$blog_id = false !== $filtered_id ? $filtered_id : $this->wpdb->blogid;
return $blog_id;
}
}

View File

@@ -0,0 +1,67 @@
<?php
class WPML_ST_Settings {
const SETTINGS_KEY = 'icl_st_settings';
/**
* @var array
*/
private $settings = null;
/**
* @var array
*/
private $updated_settings = array();
/**
* @return array
*/
public function get_settings() {
if ( ! $this->settings ) {
$options = get_option( self::SETTINGS_KEY );
$this->settings = is_array( $options ) ? $options : array();
}
return array_merge( $this->settings, $this->updated_settings );
}
/**
* @param string $name
*
* @return mixed|null
*/
public function get_setting( $name ) {
$this->get_settings();
return isset( $this->settings[ $name ] ) ? $this->settings[ $name ] : null;
}
/**
* @param string $key
* @param mixed $value
* @param bool $save
*/
public function update_setting( $key, $value, $save = false ) {
$this->get_settings();
$this->updated_settings[ $key ] = $value;
if ( $save ) {
$this->save_settings();
}
}
public function delete_settings() {
delete_option( self::SETTINGS_KEY );
}
public function save_settings() {
$settings = $this->get_settings();
update_option( self::SETTINGS_KEY, $settings );
do_action( 'icl_save_settings', $this->updated_settings );
$this->updated_settings = array();
$this->settings = $settings;
}
}

View File

@@ -0,0 +1,98 @@
<?php
class WPML_ST_String_Factory {
private $wpdb;
/**
* WPML_ST_String_Factory constructor.
*
* @param wpdb $wpdb
*/
public function __construct( wpdb $wpdb ) {
$this->wpdb = $wpdb;
}
/** @var int[] $string_id_cache */
private $string_id_cache = array();
/** @var WPML_ST_String $string_cache */
private $string_cache = array();
/**
* @param int $string_id
*
* @return WPML_ST_String
*/
public function find_by_id( $string_id ) {
$this->string_cache[ $string_id ] = isset( $this->string_cache[ $string_id ] )
? $this->string_cache[ $string_id ] : new WPML_ST_String( $string_id, $this->wpdb );
return $this->string_cache[ $string_id ];
}
/**
* @param string $name
*
* @return WPML_ST_String
*/
public function find_by_name( $name ) {
$sql = $this->wpdb->prepare( "SELECT id FROM {$this->wpdb->prefix}icl_strings WHERE name=%s LIMIT 1", $name );
$cache_key = md5( $sql );
$this->string_id_cache[ $cache_key ] = isset( $this->string_id_cache[ $cache_key ] )
? $this->string_id_cache[ $cache_key ]
: (int) $this->wpdb->get_var( $sql );
$string_id = $this->string_id_cache[ $cache_key ];
$this->string_cache[ $string_id ] = isset( $this->string_cache[ $string_id ] )
? $this->string_cache[ $string_id ] : new WPML_ST_String( $string_id, $this->wpdb );
return $this->string_cache[ $this->string_id_cache[ $cache_key ] ];
}
/**
* @param string $name
*
* @return WPML_ST_Admin_String
*/
public function find_admin_by_name( $name ) {
$sql = $this->wpdb->prepare( "SELECT id FROM {$this->wpdb->prefix}icl_strings WHERE name=%s LIMIT 1", $name );
$string_id = (int) $this->wpdb->get_var( $sql );
return new WPML_ST_Admin_String( $string_id, $this->wpdb );
}
/**
* @param string $string
* @param string|array $context
* @param bool|false $name
*
* @return mixed
*/
public function get_string_id( $string, $context, $name = false ) {
list( $domain, $gettext_context ) = wpml_st_extract_context_parameters( $context );
$sql = "SELECT id FROM {$this->wpdb->prefix}icl_strings WHERE BINARY value=%s";
$prepare_args = array( $string );
if ( $gettext_context ) {
$sql .= ' AND gettext_context=%s';
$prepare_args[] = $gettext_context;
}
if ( $domain ) {
$sql .= ' AND context=%s';
$prepare_args[] = $domain;
}
if ( $name !== false ) {
$sql .= ' AND name = %s ';
$prepare_args[] = $name;
}
$sql = $this->wpdb->prepare( $sql . ' LIMIT 1', $prepare_args );
$cache_key = md5( $sql );
$this->string_id_cache[ $cache_key ] = isset( $this->string_id_cache[ $cache_key ] )
? $this->string_id_cache[ $cache_key ]
: (int) $this->wpdb->get_var( $sql );
return $this->string_id_cache[ $cache_key ];
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* WPML_ST_String_Statuses class
*
* Get the translation status text for the given status
*/
class WPML_ST_String_Statuses {
public static function get_status( $status ) {
switch ( $status ) {
case ICL_STRING_TRANSLATION_COMPLETE:
return __( 'Translation complete', 'wpml-string-translation' );
case ICL_STRING_TRANSLATION_PARTIAL:
return __( 'Partial translation', 'wpml-string-translation' );
case ICL_STRING_TRANSLATION_NEEDS_UPDATE:
return __( 'Translation needs update', 'wpml-string-translation' );
case ICL_STRING_TRANSLATION_NOT_TRANSLATED:
return __( 'Not translated', 'wpml-string-translation' );
case ICL_STRING_TRANSLATION_WAITING_FOR_TRANSLATOR:
return __( 'Waiting for translator', 'wpml-string-translation' );
}
return '';
}
}

View File

@@ -0,0 +1,352 @@
<?php
/**
* WPML_ST_String class
*
* Low level access to string in Database
*
* NOTE: Don't use this class to process a large amount of strings as it doesn't
* do any caching, etc.
*/
class WPML_ST_String {
protected $wpdb;
private $string_id;
/** @var string $language */
private $language;
/** @var int $status */
private $status;
/** @var array|null */
private $string_properties;
/**
* @param int $string_id
* @param wpdb $wpdb
*/
public function __construct( $string_id, wpdb $wpdb ) {
$this->wpdb = $wpdb;
$this->string_id = $string_id;
}
/**
* @return int
*/
public function string_id() {
return $this->string_id;
}
/**
* @return string|null
*/
public function get_language() {
$this->language = $this->language
? $this->language
: $this->wpdb->get_var(
'SELECT language ' . $this->from_where_snippet() . ' LIMIT 1'
);
return $this->language;
}
/**
* @return string
*/
public function get_value() {
return $this->wpdb->get_var( 'SELECT value ' . $this->from_where_snippet() . ' LIMIT 1' );
}
/**
* @return int
*/
public function get_status() {
$this->status = $this->status !== null
? $this->status
: (int) $this->wpdb->get_var(
'SELECT status ' . $this->from_where_snippet() . ' LIMIT 1'
);
return $this->status;
}
/**
* @param string $language
*/
public function set_language( $language ) {
if ( $language !== $this->get_language() ) {
$this->language = $language;
$this->set_property( 'language', $language );
$this->update_status();
}
}
/**
* @return stdClass[]
*/
public function get_translation_statuses() {
/** @var array<\stdClass> $statuses */
$statuses = $this->wpdb->get_results( 'SELECT language, status, mo_string ' . $this->from_where_snippet( true ) );
foreach ( $statuses as &$status ) {
if ( ! empty( $status->mo_string ) ) {
$status->status = ICL_TM_COMPLETE;
}
unset( $status->mo_string );
}
return $statuses;
}
public function get_translations() {
return $this->wpdb->get_results( 'SELECT * ' . $this->from_where_snippet( true ) );
}
/**
* For a bulk update of all strings:
*
* @see WPML_ST_Bulk_Update_Strings_Status::run
*/
public function update_status() {
global $sitepress;
/**
* If the translation has a `mo_string`, the status of this
* translation will be set to `WPML_TM_COMPLETE`
*/
$st = $this->get_translation_statuses();
if ( $st ) {
$string_language = $this->get_language();
foreach ( $st as $t ) {
if ( $string_language != $t->language ) {
$translations[ $t->language ] = $t->status;
}
}
$active_languages = $sitepress->get_active_languages();
// If has no translation or all translations are not translated
if ( empty( $translations ) || max( $translations ) == ICL_TM_NOT_TRANSLATED ) {
$status = ICL_TM_NOT_TRANSLATED;
} elseif ( in_array( ICL_TM_WAITING_FOR_TRANSLATOR, $translations ) ) {
$status = ICL_TM_WAITING_FOR_TRANSLATOR;
} elseif ( in_array( ICL_TM_NEEDS_UPDATE, $translations ) ) {
$status = ICL_TM_NEEDS_UPDATE;
} elseif ( $this->has_less_translations_than_secondary_languages( $translations, $active_languages, $string_language ) ) {
if ( in_array( ICL_TM_COMPLETE, $translations ) ) {
$status = ICL_STRING_TRANSLATION_PARTIAL;
} else {
$status = ICL_TM_NOT_TRANSLATED;
}
} else {
if ( in_array( ICL_TM_NOT_TRANSLATED, $translations ) ) {
$status = ICL_STRING_TRANSLATION_PARTIAL;
} else {
$status = ICL_TM_COMPLETE;
}
}
} else {
$status = ICL_TM_NOT_TRANSLATED;
}
if ( $status !== $this->get_status() ) {
$this->status = $status;
$this->set_property( 'status', $status );
}
return $status;
}
/**
* @param array $translations
* @param array $active_languages
* @param string $string_language
*
* @return bool
*/
private function has_less_translations_than_secondary_languages( array $translations, array $active_languages, $string_language ) {
$active_lang_codes = array_keys( $active_languages );
$translations_in_active_langs = array_intersect( $active_lang_codes, array_keys( $translations ) );
return count( $translations_in_active_langs ) < count( $active_languages ) - intval( in_array( $string_language, $active_lang_codes, true ) );
}
/**
* @param string $language
* @param string|null $value
* @param int|bool|false $status
* @param int|null $translator_id
* @param string|int|null $translation_service
* @param int|null $batch_id
*
* @return bool|int id of the translation
*/
public function set_translation( $language, $value = null, $status = false, $translator_id = null, $translation_service = null, $batch_id = null ) {
if ( ! $this->exists() ) {
return false;
}
/** @var $ICL_Pro_Translation WPML_Pro_Translation */
global $ICL_Pro_Translation;
/** @var \stdClass $res */
$res = $this->wpdb->get_row(
$this->wpdb->prepare(
'SELECT id, value, status
' . $this->from_where_snippet( true )
. ' AND language=%s',
$language
)
);
if (
isset( $res->status ) &&
$res->status == ICL_TM_WAITING_FOR_TRANSLATOR &&
is_null( $value ) &&
! in_array( $status, [ ICL_TM_IN_PROGRESS, ICL_TM_NOT_TRANSLATED ] )
&& ! $res->value
) {
return false;
}
$translation_data = array();
if ( $translation_service ) {
$translation_data['translation_service'] = $translation_service;
}
if ( $batch_id ) {
$translation_data['batch_id'] = $batch_id;
}
if ( ! is_null( $value ) ) {
$translation_data['value'] = $value;
}
if ( $translator_id ) {
$translation_data['translator_id'] = $translator_id;
}
$translation_data = apply_filters( 'wpml_st_string_translation_before_save', $translation_data, $language, $this->string_id );
if ( $res ) {
$st_id = $res->id;
if ( $status ) {
$translation_data['status'] = $status;
} elseif ( $status === ICL_TM_NOT_TRANSLATED ) {
$translation_data['status'] = ICL_TM_NOT_TRANSLATED;
}
if ( ! empty( $translation_data ) ) {
$this->wpdb->update( $this->wpdb->prefix . 'icl_string_translations', $translation_data, array( 'id' => $st_id ) );
$this->wpdb->query(
$this->wpdb->prepare( "UPDATE {$this->wpdb->prefix}icl_string_translations SET translation_date = NOW() WHERE id = %d", $st_id )
);
}
} else {
$translation_data = array_merge(
$translation_data,
array(
'string_id' => $this->string_id,
'language' => $language,
'status' => ( $status ? $status : ICL_TM_NOT_TRANSLATED ),
)
);
$this->wpdb->insert( $this->wpdb->prefix . 'icl_string_translations', $translation_data );
$st_id = $this->wpdb->insert_id;
}
if ( $ICL_Pro_Translation ) {
$ICL_Pro_Translation->fix_links_to_translated_content( $st_id, $language, 'string' );
}
icl_update_string_status( $this->string_id );
/**
* @deprecated Use wpml_st_add_string_translation instead
*/
do_action( 'icl_st_add_string_translation', $st_id );
do_action( 'wpml_st_add_string_translation', $st_id );
return $st_id;
}
public function set_location( $location ) {
$this->set_property( 'location', $location );
}
/**
* Set string wrap tag.
* Used for SEO significance, can contain values as h1 ... h6, etc.
*
* @param string $wrap_tag Wrap tag.
*/
public function set_wrap_tag( $wrap_tag ) {
$this->set_property( 'wrap_tag', $wrap_tag );
}
/**
* @param string $property
* @param mixed $value
*/
protected function set_property( $property, $value ) {
$this->wpdb->update( $this->wpdb->prefix . 'icl_strings', array( $property => $value ), array( 'id' => $this->string_id ) );
}
/**
* @param bool $translations sets whether to use original or translations table
*
* @return string
*/
protected function from_where_snippet( $translations = false ) {
if ( $translations ) {
$id_column = 'string_id';
$table = 'icl_string_translations';
} else {
$id_column = 'id';
$table = 'icl_strings';
}
return $this->wpdb->prepare( "FROM {$this->wpdb->prefix}{$table} WHERE {$id_column}=%d", $this->string_id );
}
public function exists() {
$sql = $this->wpdb->prepare( "SELECT id FROM {$this->wpdb->prefix}icl_strings WHERE id = %d", $this->string_id );
return $this->wpdb->get_var( $sql ) > 0;
}
/** @return string|null */
public function get_context() {
return $this->get_string_properties()->context;
}
/** @return string|null */
public function get_gettext_context() {
return $this->get_string_properties()->gettext_context;
}
/** @return string|null */
public function get_name() {
return $this->get_string_properties()->name;
}
private function get_string_properties() {
if ( ! $this->string_properties ) {
$row = $this->wpdb->get_row( 'SELECT name, context, gettext_context ' . $this->from_where_snippet() . ' LIMIT 1' );
$this->string_properties = $row ? $row : (object) [
'name' => null,
'gettext_context' => null,
'context' => null,
];
}
return $this->string_properties;
}
}

View File

@@ -0,0 +1,311 @@
<?php
use WPML\API\Sanitize;
class WPML_ST_Strings {
const EMPTY_CONTEXT_LABEL = 'empty-context-domain';
/**
* @var SitePress
*/
private $sitepress;
/**
* @var WP_Query
*/
private $wp_query;
/**
* @var wpdb
*/
private $wpdb;
public function __construct( $sitepress, $wpdb, $wp_query ) {
$this->wpdb = $wpdb;
$this->sitepress = $sitepress;
$this->wp_query = $wp_query;
}
public function get_string_translations() {
$string_translations = array();
$extra_cond = '';
$active_languages = $this->sitepress->get_active_languages();
$status_filter = isset( $_GET['status'] ) ? (int) $_GET['status'] : false;
$translation_priority = isset( $_GET['translation-priority'] ) ? $_GET['translation-priority'] : false;
if ( $status_filter !== false ) {
if ( $status_filter == ICL_TM_COMPLETE ) {
$extra_cond .= ' AND s.status = ' . ICL_TM_COMPLETE;
} elseif ( $status_filter == ICL_STRING_TRANSLATION_PARTIAL ) {
$extra_cond .= ' AND s.status = ' . ICL_STRING_TRANSLATION_PARTIAL;
} elseif ( $status_filter != ICL_TM_WAITING_FOR_TRANSLATOR ) {
$extra_cond .= ' AND s.status IN (' . ICL_STRING_TRANSLATION_PARTIAL . ',' . ICL_TM_NEEDS_UPDATE . ',' . ICL_TM_NOT_TRANSLATED . ',' . ICL_TM_WAITING_FOR_TRANSLATOR . ')';
}
}
if ( $translation_priority != false ) {
if ( $translation_priority === __( 'Optional', 'sitepress' ) ) {
$extra_cond .= " AND s.translation_priority IN ( '" . esc_sql( $translation_priority ) . "', '' ) ";
} else {
$extra_cond .= " AND s.translation_priority = '" . esc_sql( $translation_priority ) . "' ";
}
}
if ( array_key_exists( 'context', $_GET ) ) {
$context = Sanitize::stringProp( 'context', $_GET );
if ( self::EMPTY_CONTEXT_LABEL === $context ) {
$context = '';
}
}
if ( isset( $context ) ) {
$extra_cond .= " AND s.context = '" . esc_sql( $context ) . "'";
}
if ( $this->must_show_all_results() ) {
$limit = 9999;
$offset = 0;
} else {
$limit = $this->get_strings_per_page();
$_GET['paged'] = isset( $_GET['paged'] ) ? $_GET['paged'] : 1;
$offset = ( $_GET['paged'] - 1 ) * $limit;
}
$search_filter = $this->get_search_filter();
$joins = [];
$sql_query = ' WHERE 1 ';
if ( $status_filter === ICL_TM_WAITING_FOR_TRANSLATOR ) {
$sql_query .= ' AND s.status = ' . ICL_TM_WAITING_FOR_TRANSLATOR;
} elseif ( $active_languages && $search_filter && ! $this->must_show_all_results() ) {
$sql_query .= ' AND ' . $this->get_value_search_query();
$joins[] = "LEFT JOIN {$this->wpdb->prefix}icl_string_translations str ON str.string_id = s.id";
}
$res = $this->get_results( $sql_query, $extra_cond, $offset, $limit, $joins );
if ( $res ) {
$extra_cond = '';
if ( isset( $_GET['translation_language'] ) ) {
$extra_cond .= " AND language='" . esc_sql( $_GET['translation_language'] ) . "'";
}
foreach ( $res as $row ) {
$string_translations[ $row['string_id'] ] = $row;
$tr = $this->wpdb->get_results(
$this->wpdb->prepare(
"
SELECT id, language, status, value, mo_string, translator_id, translation_date
FROM {$this->wpdb->prefix}icl_string_translations
WHERE string_id=%d {$extra_cond}
",
$row['string_id']
),
ARRAY_A
);
if ( $tr ) {
foreach ( $tr as $t ) {
$string_translations[ $row['string_id'] ]['translations'][ $t['language'] ] = $t;
}
}
}
}
return WPML\ST\Basket\Status::add( $string_translations, array_keys( $active_languages ) );
}
/**
* @return string
*/
private function get_value_search_query() {
$language_where = wpml_collect(
[
$this->get_original_value_filter_sql(),
$this->get_name_filter_sql(),
$this->get_context_filter_sql(),
]
);
$search_context = $this->get_search_context_filter();
if ( $search_context['translation'] ) {
$language_where->push( $this->get_translation_value_filter_sql() );
}
if ( $search_context['mo_string'] ) {
$language_where->push( $this->get_mo_file_value_filter_sql() );
}
return sprintf( '((%s))', $language_where->implode( ') OR (' ) );
}
/**
* @return string
*/
private function get_original_value_filter_sql() {
return $this->get_column_filter_sql( 's.value', $this->get_search_filter(), $this->is_exact_match() );
}
/**
* @return string
*/
private function get_name_filter_sql() {
return $this->get_column_filter_sql( 's.name', $this->get_search_filter(), $this->is_exact_match() );
}
/**
* @return string
*/
private function get_context_filter_sql() {
return $this->get_column_filter_sql( 's.gettext_context', $this->get_search_filter(), $this->is_exact_match() );
}
/**
* @return string
*/
private function get_translation_value_filter_sql() {
return $this->get_column_filter_sql( 'str.value', $this->get_search_filter(), $this->is_exact_match() );
}
/**
* @return string
*/
private function get_mo_file_value_filter_sql() {
return $this->get_column_filter_sql( 'str.mo_string', $this->get_search_filter(), $this->is_exact_match() );
}
/**
* @param string $column
* @param string|null $search_filter
* @param bool|null $exact_match
*
* @return string
*/
private function get_column_filter_sql( $column, $search_filter, $exact_match ) {
$pattern = '{column} LIKE \'%{value}%\'';
if ( $exact_match ) {
$pattern = '{column} = \'{value}\'';
}
return str_replace(
array( '{column}', '{value}' ),
array(
esc_sql( $column ),
esc_sql( str_replace( "'", "&#039;", $search_filter ) ),
),
$pattern
);
}
public function get_per_domain_counts( $status ) {
$extra_cond = '';
if ( $status !== false ) {
if ( $status == ICL_TM_COMPLETE ) {
$extra_cond .= ' AND s.status = ' . ICL_TM_COMPLETE;
} else {
$extra_cond .= ' AND s.status IN (' . ICL_STRING_TRANSLATION_PARTIAL . ',' . ICL_TM_NEEDS_UPDATE . ',' . ICL_TM_NOT_TRANSLATED . ')';
}
}
$results = $this->wpdb->get_results(
"
SELECT context, COUNT(context) AS c
FROM {$this->wpdb->prefix}icl_strings s
WHERE 1 {$extra_cond} AND TRIM(s.value) <> ''
GROUP BY context
ORDER BY context ASC"
);
return $results;
}
private function get_strings_per_page() {
$st_settings = $this->sitepress->get_setting( 'st' );
return isset( $st_settings['strings_per_page'] ) ? $st_settings['strings_per_page'] : WPML_ST_DEFAULT_STRINGS_PER_PAGE;
}
private function get_results( $where_snippet, $extra_cond, $offset, $limit, $joins = array(), $selects = array() ) {
$query = $this->build_sql_start( $selects, $joins );
$query .= $where_snippet;
$query .= " {$extra_cond} ";
$query .= $this->filter_empty_order_snippet( $offset, $limit );
$res = $this->wpdb->get_results( $query, ARRAY_A );
$this->set_pagination_counts( $limit );
return $res;
}
private function filter_empty_order_snippet( $offset, $limit ) {
return " AND TRIM(s.value) <> '' ORDER BY string_id DESC LIMIT {$offset},{$limit}";
}
private function set_pagination_counts( $limit ) {
if ( ! is_null( $this->wp_query ) ) {
$this->wp_query->found_posts = $this->wpdb->get_var( 'SELECT FOUND_ROWS()' );
$this->wp_query->query_vars['posts_per_page'] = $limit;
$this->wp_query->max_num_pages = ceil( $this->wp_query->found_posts / $limit );
}
}
private function build_sql_start( $selects = array(), $joins = array() ) {
array_unshift( $selects, 'SQL_CALC_FOUND_ROWS DISTINCT(s.id) AS string_id, s.language AS string_language, s.string_package_id, s.context, s.gettext_context, s.name, s.value, s.status AS status, s.translation_priority' );
return 'SELECT ' . implode( ', ', $selects ) . " FROM {$this->wpdb->prefix}icl_strings s " . implode( PHP_EOL, $joins ) . ' ';
}
/**
* @return string|bool
*/
private function get_search_filter() {
if ( array_key_exists( 'search', $_GET ) ) {
return stripcslashes( $_GET['search'] );
}
return false;
}
/**
* @return bool
*/
private function is_exact_match() {
if ( array_key_exists( 'em', $_GET ) ) {
return (int) $_GET['em'] === 1;
}
return false;
}
/**
* @return array
*/
private function get_search_context_filter() {
$result = array(
'original' => true,
'translation' => false,
'mo_string' => false,
);
if ( array_key_exists( 'search_translation', $_GET ) && ! $this->must_show_all_results() ) {
$result['translation'] = (bool) $_GET['search_translation'];
$result['mo_string'] = (bool) $_GET['search_translation'];
}
return $result;
}
/**
* @return bool
*/
private function must_show_all_results() {
return isset( $_GET['show_results'] ) && $_GET['show_results'] === 'all';
}
}

View File

@@ -0,0 +1,210 @@
<?php
class WPML_ST_User_Fields {
/**
* @var string
*/
private $context = 'Authors';
/** @var SitePress */
private $sitepress;
/**
* @var mixed|WP_User|null
*/
private $authordata;
/** @var bool */
private $lock_get_the_author_filter;
public function __construct( SitePress $sitepress, &$authordata ) {
$this->authordata = &$authordata;
$this->sitepress = $sitepress;
}
public function init_hooks() {
if ( ! is_admin() ) {
add_action( 'init', array( $this, 'add_get_the_author_field_filters' ) );
add_filter( 'the_author', array( $this, 'the_author_filter' ), 10, 2 );
}
add_action( 'profile_update', array( $this, 'profile_update_action' ), 10 );
add_action( 'user_register', array( $this, 'profile_update_action' ), 10 );
}
public function add_get_the_author_field_filters() {
$translatable_fields = $this->get_translatable_meta_fields();
foreach ( $translatable_fields as $field ) {
add_filter( "get_the_author_{$field}", array( $this, 'get_the_author_field_filter' ), 10, 3 );
}
}
/**
* @param int $user_id
*/
public function profile_update_action( $user_id ) {
$this->register_user_strings( $user_id );
}
/**
* @param int $user_id
*/
private function register_user_strings( $user_id ) {
if ( $this->is_user_role_translatable( $user_id ) ) {
$fields = $this->get_translatable_meta_fields();
foreach ( $fields as $field ) {
$name = $this->get_string_name( $field, $user_id );
$value = get_user_meta( $user_id, $field, true );
/**
* Some fields like "display_name" are not part of user meta
* so we have a fallback to get its value from `get_the_author_meta`
*/
if ( '' === $value ) {
$value = get_the_author_meta( $field, $user_id );
}
icl_register_string( $this->context, $name, $value, true );
}
}
}
/**
* @param string $value
* @param int $user_id
* @param int $original_user_id
*
* @return string
*/
public function get_the_author_field_filter( $value, $user_id, $original_user_id ) {
if ( $this->lock_get_the_author_filter ) {
return $value;
}
$field = preg_replace( '/get_the_author_/', '', current_filter(), 1 );
$value = $this->translate_user_meta_field( $field, $value, $user_id );
return $this->apply_filters_for_the_author_field_output( $value, $field, $user_id, $original_user_id );
}
/**
* @param string $value
* @param string $field
* @param int $user_id
* @param int $original_user_id
*
* @return string
*/
private function apply_filters_for_the_author_field_output( $value, $field, $user_id, $original_user_id ) {
$this->lock_get_the_author_filter = true;
/**
* WP hook described in wp-includes/author-template.php
*
* @see get_the_author_meta
*/
$value = apply_filters( "get_the_author_$field", $value, $user_id, $original_user_id );
$this->lock_get_the_author_filter = false;
return $value;
}
/**
* This filter will only replace the "display_name" of the current author (in global $authordata)
*
* @param mixed|string|null $value
*
* @return mixed|string|null
*/
public function the_author_filter( $value ) {
if ( isset( $this->authordata->ID ) ) {
$value = $this->translate_user_meta_field( 'display_name', $value, $this->authordata->ID );
}
return $value;
}
/**
* @param string $field
* @param string $value
* @param mixed|int|null $user_id
*
* @return string
*/
private function translate_user_meta_field( $field, $value, $user_id = null ) {
if ( ! is_admin() && $this->is_user_role_translatable( $user_id ) ) {
$name = $this->get_string_name( $field, $user_id );
$value = icl_translate( $this->context, $name, $value, true );
}
return $value;
}
/**
* @return array
*/
private function get_translatable_meta_fields() {
$default_fields = array(
'first_name',
'last_name',
'nickname',
'description',
'display_name',
);
return apply_filters( 'wpml_translatable_user_meta_fields', $default_fields );
}
/**
* @param int $user_id
*
* @return bool
*/
public function is_user_role_translatable( $user_id ) {
$ret = false;
$translated_roles = $this->get_translated_roles();
$user = new WP_User( $user_id );
if ( is_array( $user->roles ) && array_intersect( $user->roles, $translated_roles ) ) {
$ret = true;
}
return $ret;
}
/**
* @return array
*/
private function get_translated_roles() {
$st_settings = $this->sitepress->get_setting( 'st' );
return isset( $st_settings['translated-users'] ) && is_array( $st_settings['translated-users'] )
? $st_settings['translated-users'] : array();
}
/**
* @param string $field
* @param int $user_id
*
* @return string
*/
private function get_string_name( $field, $user_id ) {
return $field . '_' . $user_id;
}
/**
* @return array
*/
public function init_register_strings() {
$processed_ids = array();
$translated_roles = $this->get_translated_roles();
$blog_id = get_current_blog_id();
foreach ( $translated_roles as $role ) {
$args = array(
'blog_id' => $blog_id,
'fields' => 'ID',
'exclude' => $processed_ids,
'role' => $role,
);
$users = get_users( $args );
foreach ( $users as $user_id ) {
$this->register_user_strings( $user_id );
$processed_ids[] = $user_id;
}
}
return $processed_ids;
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* Class WPML_ST_Verify_Dependencies
*
* Checks that the WPML Core plugin is installed and satisfies certain version
* requirements
*/
class WPML_ST_Verify_Dependencies {
/**
* @param string $wpml_core_version
*/
function verify_wpml( $wpml_core_version ) {
if ( false === $wpml_core_version ) {
add_action(
'admin_notices',
array(
$this,
'notice_no_wpml',
)
);
} elseif ( version_compare( $wpml_core_version, '3.5', '<' ) ) {
add_action( 'admin_notices', array( $this, 'wpml_is_outdated' ) );
}
}
function notice_no_wpml() {
?>
<div class="error wpml-admin-notice wpml-st-inactive wpml-inactive">
<p><?php esc_html_e( 'Please activate WPML Multilingual CMS to have WPML String Translation working.', 'wpml-string-translation' ); ?></p>
</div>
<?php
}
function wpml_is_outdated() {
?>
<div
class="message error wpml-admin-notice wpml-st-inactive wpml-outdated">
<p><?php esc_html_e( 'WPML String Translation is enabled but not effective, because WPML is outdated. Please update WPML first.', 'wpml-string-translation' ); ?></p>
</div>
<?php
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* @author OnTheGo Systems
*/
namespace WPML\ST\Container;
class Config {
public static function getSharedClasses() {
return [
\WPML\ST\StringsCleanup\UntranslatedStrings::class,
\WPML\ST\Gettext\AutoRegisterSettings::class,
\WPML\ST\Gettext\Hooks::class,
\WPML\ST\Gettext\Settings::class,
\WPML\ST\MO\LoadedMODictionary::class,
\WPML\ST\MO\File\Manager::class,
\WPML\ST\MO\File\Builder::class,
\WPML\ST\Package\Domains::class,
\WPML\ST\StringsFilter\Provider::class,
\WPML\ST\TranslationFile\Domains::class,
\WPML_String_Translation::class,
\WPML_ST_Blog_Name_And_Description_Hooks::class,
\WPML_ST_Settings::class,
\WPML_ST_String_Factory::class,
\WPML_ST_Upgrade::class,
\WPML_Theme_Localization_Type::class,
\WPML_ST_Translations_File_Dictionary_Storage_Table::class,
\WPML\ST\TranslationFile\Sync\TranslationUpdates::class,
];
}
public static function getAliases() {
return [
\WPML_ST_Translations_File_Dictionary_Storage::class => \WPML_ST_Translations_File_Dictionary_Storage_Table::class,
];
}
public static function getDelegated() {
return [
\WPML_Admin_Texts::class => function() {
return wpml_st_load_admin_texts(); },
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace WPML\ST\DB\Mappers;
use WPML\FP\Curryable;
/**
* Class DomainsRepository
* @package WPML\ST\DB\Mappers
*
* @method static callable|array getByStringIds( ...$stringIds ) - Curried :: int[]->string[]
*
*/
class DomainsRepository {
use Curryable;
public static function init() {
self::curryN( 'getByStringIds', 1, function ( array $stringIds ) {
global $wpdb;
$sql = "SELECT DISTINCT `context` FROM {$wpdb->prefix}icl_strings WHERE id IN (" . wpml_prepare_in( $stringIds ) . ")";
return $wpdb->get_col( $sql );
} );
}
}
DomainsRepository::init();

View File

@@ -0,0 +1,16 @@
<?php
namespace WPML\ST\DB\Mappers;
use function WPML\Container\make;
use function WPML\FP\partial;
class Hooks implements \IWPML_Action, \IWPML_Backend_Action, \IWPML_Frontend_Action {
public function add_hooks() {
$getStringById = [ make( \WPML_ST_DB_Mappers_Strings::class ), 'getById' ];
$moveStringToDomain = partial( [ Update::class, 'moveStringToDomain' ], $getStringById );
add_action( 'wpml_st_move_string_to_domain', $moveStringToDomain, 10, 2 );
add_action( 'wpml_st_move_all_strings_to_new_domain', [ Update::class, 'moveAllStringsToNewDomain' ], 10, 2 );
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace WPML\ST\DB\Mappers;
use function WPML\FP\curryN;
class StringTranslations {
/**
* @param \wpdb $wpdb
* @param int $stringId
* @param string $language
*
* @return callable|bool
*/
public static function hasTranslation( $wpdb = null, $stringId = null, $language = null ) {
$has = function ( \wpdb $wpdb, $stringId, $language ) {
$sql = "SELECT COUNT(id) FROM {$wpdb->prefix}icl_string_translations WHERE string_id = %d AND language = %s";
return $wpdb->get_var( $wpdb->prepare( $sql, $stringId, $language ) ) > 0;
};
return call_user_func_array( curryN( 3, $has ), func_get_args() );
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace WPML\ST\DB\Mappers;
use \wpdb;
use \WPML_DB_Chunk;
class StringsRetrieve {
/** @var wpdb $wpdb */
private $wpdb;
/** @var WPML_DB_Chunk $chunk_retrieve */
private $chunk_retrieve;
public function __construct( wpdb $wpdb, WPML_DB_Chunk $chunk_retrieve ) {
$this->wpdb = $wpdb;
$this->chunk_retrieve = $chunk_retrieve;
}
/**
* @param string $language
* @param string $domain
* @param bool $modified_mo_only
*
* @return array
*/
public function get( $language, $domain, $modified_mo_only = false ) {
$args = [ $language, $language, $domain ];
$query = "
SELECT
s.id,
st.status,
s.domain_name_context_md5 AS ctx ,
st.value AS translated,
st.mo_string AS mo_string,
s.value AS original,
s.gettext_context,
s.name
FROM {$this->wpdb->prefix}icl_strings s
" . $this->getStringTranslationJoin() . '
' . $this->getDomainWhere();
if ( $modified_mo_only ) {
$query .= $this->getModifiedMOOnlyWhere();
}
$total_strings = $this->get_number_of_strings_in_domain( $language, $domain, $modified_mo_only );
return $this->chunk_retrieve->retrieve( $query, $args, $total_strings );
}
/**
* @param string $language
* @param string $domain
* @param bool $modified_mo_only
*
* @return int
*/
private function get_number_of_strings_in_domain( $language, $domain, $modified_mo_only ) {
$tables = "SELECT COUNT(s.id) FROM {$this->wpdb->prefix}icl_strings AS s";
$where = $this->wpdb->prepare( $this->getDomainWhere(), [ $domain ] );
if ( $modified_mo_only ) {
$tables .= $this->wpdb->prepare( $this->getStringTranslationJoin(), [ $language, $language ] );
$where .= $this->getModifiedMOOnlyWhere();
}
return (int) $this->wpdb->get_var( $tables . $where );
}
/**
* @return string
*/
private function getStringTranslationJoin() {
return " LEFT JOIN {$this->wpdb->prefix}icl_string_translations AS st
ON s.id = st.string_id
AND st.language = %s
AND s.language != %s";
}
/** @return string */
private function getDomainWhere() {
return ' WHERE UPPER(context) = UPPER(%s)';
}
/** @return string */
private function getModifiedMOOnlyWhere() {
return ' AND st.status IN (' .
wpml_prepare_in( [ ICL_TM_COMPLETE, ICL_TM_NEEDS_UPDATE ], '%d' ) .
') AND st.value IS NOT NULL';
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace WPML\ST\DB\Mappers;
class Update {
/**
* @param callable $getStringById
* @param int $stringId
* @param string $domain
*
* @return bool
*/
public static function moveStringToDomain( callable $getStringById, $stringId, $domain ) {
global $wpdb;
$string = $getStringById( $stringId );
if ( $string ) {
$wpdb->update( $wpdb->prefix . 'icl_strings', [ 'context' => $domain ], [ 'id' => $stringId ] );
self::regenerateMOFiles( $string->context, $domain );
return true;
}
return false;
}
/**
* @param string $oldDomain
* @param string $newDomain
*
* @return int
*/
public static function moveAllStringsToNewDomain( $oldDomain, $newDomain ) {
global $wpdb;
$affected = (int) $wpdb->update(
$wpdb->prefix . 'icl_strings',
[ 'context' => $newDomain ],
[ 'context' => $oldDomain ]
);
if ( $affected ) {
self::regenerateMOFiles( $oldDomain, $newDomain );
}
return $affected;
}
private static function regenerateMOFiles( $oldDomain, $newDomain ) {
do_action( 'wpml_st_refresh_domain', $oldDomain );
do_action( 'wpml_st_refresh_domain', $newDomain );
}
}

View File

@@ -0,0 +1,102 @@
<?php
class WPML_ST_Bulk_Strings_Insert_Exception extends Exception {
}
class WPML_ST_Bulk_Strings_Insert {
/** @var wpdb */
private $wpdb;
/** @var int */
private $chunk_size = 1000;
/**
* @param wpdb $wpdb
*/
public function __construct( wpdb $wpdb ) {
$this->wpdb = $wpdb;
}
/**
* @param int $chunk_size
*/
public function set_chunk_size( $chunk_size ) {
$this->chunk_size = $chunk_size;
}
/**
* @param WPML_ST_Models_String[] $strings
*/
public function insert_strings( array $strings ) {
foreach ( array_chunk( $strings, $this->chunk_size ) as $chunk ) {
$query = "INSERT IGNORE INTO {$this->wpdb->prefix}icl_strings "
. '(`language`, `context`, `gettext_context`, `domain_name_context_md5`, `name`, `value`, `status`) VALUES ';
$query .= implode( ',', array_map( array( $this, 'build_string_row' ), $chunk ) );
$this->wpdb->suppress_errors = true;
$this->wpdb->query( $query );
$this->wpdb->suppress_errors = false;
if ( $this->wpdb->last_error ) {
throw new WPML_ST_Bulk_Strings_Insert_Exception( 'Deadlock with bulk insert' );
}
}
}
/**
* @param WPML_ST_Models_String_Translation[] $translations
*/
public function insert_string_translations( array $translations ) {
foreach ( array_chunk( $translations, $this->chunk_size ) as $chunk ) {
$query = "INSERT IGNORE INTO {$this->wpdb->prefix}icl_string_translations "
. '(`string_id`, `language`, `status`, `mo_string`) VALUES ';
$query .= implode( ',', array_map( array( $this, 'build_translation_row' ), $chunk ) );
$query .= ' ON DUPLICATE KEY UPDATE `mo_string`=VALUES(`mo_string`)';
$this->wpdb->suppress_errors = true;
$this->wpdb->query( $query );
$this->wpdb->suppress_errors = false;
if ( $this->wpdb->last_error ) {
throw new WPML_ST_Bulk_Strings_Insert_Exception( 'Deadlock with bulk insert' );
}
}
}
/**
* @param WPML_ST_Models_String $string
*
* @return string
*/
private function build_string_row( WPML_ST_Models_String $string ) {
return $this->wpdb->prepare(
'(%s, %s, %s, %s, %s, %s, %d)',
$string->get_language(),
$string->get_domain(),
$string->get_context(),
$string->get_domain_name_context_md5(),
$string->get_name(),
$string->get_value(),
$string->get_status()
);
}
/**
* @param WPML_ST_Models_String_Translation $translation
*
* @return string
*/
private function build_translation_row( WPML_ST_Models_String_Translation $translation ) {
return $this->wpdb->prepare(
'(%d, %s, %d, %s)',
$translation->get_string_id(),
$translation->get_language(),
$translation->get_status(),
$translation->get_mo_string()
);
}
}

View File

@@ -0,0 +1,230 @@
<?php
class WPML_ST_Bulk_Update_Strings_Status {
/** @var wpdb $wpdb */
private $wpdb;
/** @var array $active_lang_codes */
private $active_lang_codes;
public function __construct( wpdb $wpdb, array $active_lang_codes ) {
$this->wpdb = $wpdb;
$this->active_lang_codes = $active_lang_codes;
}
/**
* This bulk process was transposed from PHP code
*
* @see WPML_ST_String::update_status
*
* Important: The order we call each method is important because it reflects
* the order of the conditions in WPML_ST_String::update_status. The updated IDs
* will not be updated anymore in the subsequent calls.
*
* @return array updated IDs
*/
public function run() {
$updated_ids = $this->update_strings_with_no_translation();
$updated_ids = $this->update_strings_with_all_translations_not_translated( $updated_ids );
$updated_ids = $this->update_strings_with_one_translation_waiting_for_translator( $updated_ids );
$updated_ids = $this->update_strings_with_one_translation_needs_update( $updated_ids );
$updated_ids = $this->update_strings_with_less_translations_than_langs_and_one_translation_completed( $updated_ids );
$updated_ids = $this->update_strings_with_less_translations_than_langs_and_no_translation_completed( $updated_ids );
$updated_ids = $this->update_remaining_strings_with_one_not_translated( $updated_ids );
$updated_ids = $this->update_remaining_strings( $updated_ids );
return $updated_ids;
}
/**
* @return array
*/
private function update_strings_with_no_translation() {
$ids = $this->wpdb->get_col(
"SELECT DISTINCT s.id FROM {$this->wpdb->prefix}icl_strings AS s
LEFT JOIN {$this->wpdb->prefix}icl_string_translations AS st ON st.string_id = s.id
WHERE st.string_id IS NULL"
);
$this->update_strings_status( $ids, ICL_TM_NOT_TRANSLATED );
return $ids;
}
/**
* @param array $updated_ids
*
* @return array
*/
private function update_strings_with_all_translations_not_translated( array $updated_ids ) {
$subquery_not_exists = $this->get_translations_snippet()
. $this->wpdb->prepare( " AND (st.status != %d OR (st.mo_string != '' AND st.mo_string IS NOT NULL))", ICL_TM_NOT_TRANSLATED );
$ids = $this->wpdb->get_col(
"SELECT DISTINCT s.id FROM {$this->wpdb->prefix}icl_strings AS s
WHERE NOT EXISTS(" . $subquery_not_exists . ')'
. $this->get_and_not_in_updated_snippet( $updated_ids )
);
$this->update_strings_status( $ids, ICL_TM_NOT_TRANSLATED );
return array_merge( $updated_ids, $ids );
}
/**
* @param array $updated_ids
*
* @return array
*/
private function update_strings_with_one_translation_waiting_for_translator( array $updated_ids ) {
$subquery = $this->get_translations_snippet()
. $this->wpdb->prepare( ' AND st.status = %d', ICL_TM_WAITING_FOR_TRANSLATOR );
return $this->update_string_ids_if_subquery_exists( $subquery, $updated_ids, ICL_TM_WAITING_FOR_TRANSLATOR );
}
/**
* @param array $updated_ids
*
* @return array
*/
private function update_strings_with_one_translation_needs_update( array $updated_ids ) {
$subquery = $this->get_translations_snippet()
. $this->wpdb->prepare( ' AND st.status = %d', ICL_TM_NEEDS_UPDATE );
return $this->update_string_ids_if_subquery_exists( $subquery, $updated_ids, ICL_TM_NEEDS_UPDATE );
}
/**
* @param array $updated_ids
*
* @return array
*/
private function update_strings_with_less_translations_than_langs_and_one_translation_completed( array $updated_ids ) {
$subquery = $this->get_translations_snippet()
. $this->wpdb->prepare( " AND (st.status = %d OR (st.mo_string != '' AND st.mo_string IS NOT NULL))", ICL_TM_COMPLETE )
. $this->get_and_translations_less_than_secondary_languages_snippet();
return $this->update_string_ids_if_subquery_exists( $subquery, $updated_ids, ICL_STRING_TRANSLATION_PARTIAL );
}
/**
* @param array $updated_ids
*
* @return array
*/
private function update_strings_with_less_translations_than_langs_and_no_translation_completed( array $updated_ids ) {
$subquery = $this->get_translations_snippet()
. $this->wpdb->prepare( ' AND st.status != %d', ICL_TM_COMPLETE )
. $this->get_and_translations_less_than_secondary_languages_snippet();
return $this->update_string_ids_if_subquery_exists( $subquery, $updated_ids, ICL_TM_NOT_TRANSLATED );
}
/**
* Defaults to ICL_STRING_TRANSLATION_PARTIAL if not caught before
*
* @param array $updated_ids
*
* @return array
*/
private function update_remaining_strings_with_one_not_translated( array $updated_ids ) {
$subquery = $this->get_translations_snippet()
. $this->wpdb->prepare( " AND st.status = %d AND (st.mo_string = '' OR st.mo_string IS NULL)", ICL_TM_NOT_TRANSLATED );
return $this->update_string_ids_if_subquery_exists( $subquery, $updated_ids, ICL_STRING_TRANSLATION_PARTIAL );
}
/**
* Defaults to ICL_TM_COMPLETE if not caught before
*
* @param array $updated_ids
*
* @return array
*/
private function update_remaining_strings( array $updated_ids ) {
$subquery = $this->get_translations_snippet();
return $this->update_string_ids_if_subquery_exists( $subquery, $updated_ids, ICL_TM_COMPLETE );
}
/**
* @param string $subquery
* @param array $updated_ids
* @param int $new_status
*
* @return array
*/
private function update_string_ids_if_subquery_exists( $subquery, array $updated_ids, $new_status ) {
$ids = $this->wpdb->get_col(
"SELECT DISTINCT s.id FROM {$this->wpdb->prefix}icl_strings AS s
WHERE EXISTS(" . $subquery . ')'
. $this->get_and_not_in_updated_snippet( $updated_ids )
);
$this->update_strings_status( $ids, $new_status );
return array_merge( $updated_ids, $ids );
}
/**
* Subquery for the string translations
*
* @return string
*/
private function get_translations_snippet() {
return "SELECT DISTINCT st.string_id
FROM {$this->wpdb->prefix}icl_string_translations AS st
WHERE st.string_id = s.id
AND st.language != s.language";
}
/**
* Subquery where translations are less than the number of secondary languages:
* - the string translation language must be different than the string language
* - the string translation language must be part of the active languages
*
* @return string
*/
private function get_and_translations_less_than_secondary_languages_snippet() {
$secondary_languages_count = count( $this->active_lang_codes ) - 1;
return $this->wpdb->prepare(
" AND (
SELECT COUNT( st2.id )
FROM {$this->wpdb->prefix}icl_string_translations AS st2
WHERE st2.string_id = s.id
AND st2.language != s.language
AND st2.language IN(" . wpml_prepare_in( $this->active_lang_codes ) . ')
) < %d',
$secondary_languages_count
);
}
/**
* @param array $updated_ids
*
* @return string
*/
private function get_and_not_in_updated_snippet( array $updated_ids ) {
return ' AND s.id NOT IN(' . wpml_prepare_in( $updated_ids ) . ')';
}
/**
* @param array $ids
* @param int $status
*/
private function update_strings_status( array $ids, $status ) {
if ( ! $ids ) {
return;
}
$this->wpdb->query(
$this->wpdb->prepare(
"UPDATE {$this->wpdb->prefix}icl_strings SET status = %d WHERE id IN(" . wpml_prepare_in( $ids ) . ')',
$status
)
);
}
}

View File

@@ -0,0 +1,77 @@
<?php
class WPML_ST_DB_Mappers_String_Positions {
/**
* @var wpdb
*/
private $wpdb;
/**
* @param wpdb $wpdb
*/
public function __construct( wpdb $wpdb ) {
$this->wpdb = $wpdb;
}
/**
* @param int $string_id
* @param int $kind
*
* @return int
*/
public function get_count_of_positions_by_string_and_kind( $string_id, $kind ) {
$query = "
SELECT COUNT(id)
FROM {$this->wpdb->prefix}icl_string_positions
WHERE string_id = %d AND kind = %d
";
return (int) $this->wpdb->get_var( $this->wpdb->prepare( $query, $string_id, $kind ) );
}
/**
* @param int $string_id
* @param int $kind
*
* @return array
*/
public function get_positions_by_string_and_kind( $string_id, $kind ) {
$query = "
SELECT position_in_page
FROM {$this->wpdb->prefix}icl_string_positions
WHERE string_id = %d AND kind = %d
";
return $this->wpdb->get_col( $this->wpdb->prepare( $query, $string_id, $kind ) );
}
/**
* @param int $string_id
* @param string $position
* @param int $kind
*
* @return bool
*/
public function is_string_tracked( $string_id, $position, $kind ) {
$query = "
SELECT id
FROM {$this->wpdb->prefix}icl_string_positions
WHERE string_id=%d AND position_in_page=%s AND kind=%s
";
return (bool) $this->wpdb->get_var( $this->wpdb->prepare( $query, $string_id, $position, $kind ) );
}
/**
* @param int $string_id
* @param string $position
* @param int $kind
*/
public function insert( $string_id, $position, $kind ) {
$this->wpdb->insert( $this->wpdb->prefix . 'icl_string_positions', array(
'string_id' => $string_id,
'kind' => $kind,
'position_in_page' => $position,
) );
}
}

View File

@@ -0,0 +1,59 @@
<?php
class WPML_ST_DB_Mappers_Strings {
/**
* @var wpdb
*/
private $wpdb;
/**
* @param wpdb $wpdb
*/
public function __construct( wpdb $wpdb ) {
$this->wpdb = $wpdb;
}
/**
* @param string $context
*
* @return array
*/
public function get_all_by_context( $context ) {
$where = strpos( $context, '%' ) === false ? '=' : 'LIKE';
$query = "
SELECT * FROM {$this->wpdb->prefix}icl_strings
WHERE context {$where} %s
";
$query = $this->wpdb->prepare( $query, esc_sql( $context ) );
return $this->wpdb->get_results( $query, ARRAY_A );
}
/**
* Get a single string row by its domain and value
*
* @param string $domain
* @param string $value
*
* @return array
*/
public function getByDomainAndValue( $domain, $value ) {
$sql = "SELECT * FROM {$this->wpdb->prefix}icl_strings WHERE `context` = %s and `value` = %s";
return $this->wpdb->get_row( $this->wpdb->prepare( $sql, $domain, $value ) );
}
/**
* Get a single string row by its id
*
* @param int $id
*
* @return array
*/
public function getById( $id ) {
$sql = "SELECT * FROM {$this->wpdb->prefix}icl_strings WHERE id = %d";
return $this->wpdb->get_row( $this->wpdb->prepare( $sql, $id ) );
}
}

View File

@@ -0,0 +1,67 @@
<?php
class WPML_ST_Models_String_Translation {
/** @var int */
private $string_id;
/** @var string */
private $language;
/** @var int */
private $status;
/** @var string */
private $value;
/** @var string */
private $mo_string;
/**
* @param int $string_id
* @param string $language
* @param int $status
* @param string $value
*/
public function __construct( $string_id, $language, $status, $value, $mo_string ) {
$this->string_id = (int) $string_id;
$this->language = (string) $language;
$this->status = (int) $status;
$this->value = (string) $value;
$this->mo_string = (string) $mo_string;
}
/**
* @return int
*/
public function get_string_id() {
return $this->string_id;
}
/**
* @return string
*/
public function get_language() {
return $this->language;
}
/**
* @return int
*/
public function get_status() {
return $this->status;
}
/**
* @return string
*/
public function get_value() {
return $this->value;
}
/**
* @return string
*/
public function get_mo_string() {
return $this->mo_string;
}
}

View File

@@ -0,0 +1,96 @@
<?php
class WPML_ST_Models_String {
/** @var string */
private $language;
/** @var string */
private $domain;
/** @var string */
private $context;
/** @var string */
private $value;
/** @var int */
private $status;
/** @var string */
private $name;
/** @var string */
private $domain_name_context_md5;
/**
* @param string $language
* @param string $domain
* @param string $context
* @param string $value
* @param int $status
* @param string|null $name
*/
public function __construct( $language, $domain, $context, $value, $status, $name = null ) {
$this->language = (string) $language;
$this->domain = (string) $domain;
$this->context = (string) $context;
$this->value = (string) $value;
$this->status = (int) $status;
if ( ! $name ) {
$name = md5( $value );
}
$this->name = (string) $name;
$this->domain_name_context_md5 = md5( $domain . $name . $context );
}
/**
* @return string
*/
public function get_language() {
return $this->language;
}
/**
* @return string
*/
public function get_domain() {
return $this->domain;
}
/**
* @return string
*/
public function get_context() {
return $this->context;
}
/**
* @return string
*/
public function get_value() {
return $this->value;
}
/**
* @return int
*/
public function get_status() {
return $this->status;
}
/**
* @return string
*/
public function get_name() {
return $this->name;
}
/**
* @return string
*/
public function get_domain_name_context_md5() {
return $this->domain_name_context_md5;
}
}

View File

@@ -0,0 +1,147 @@
<?php
class WPML_ST_Word_Count_Package_Records {
/** @var wpdb */
private $wpdb;
public function __construct( wpdb $wpdb ) {
$this->wpdb = $wpdb;
}
/** @return array */
public function get_all_package_ids() {
return array_map(
'intval',
$this->wpdb->get_col( "SELECT ID FROM {$this->wpdb->prefix}icl_string_packages" )
);
}
/** @return array */
public function get_packages_ids_without_word_count() {
return array_map(
'intval',
$this->wpdb->get_col(
"SELECT ID FROM {$this->wpdb->prefix}icl_string_packages WHERE word_count IS NULL"
)
);
}
/** @return array */
public function get_word_counts( $post_id ) {
return $this->wpdb->get_col(
$this->wpdb->prepare(
"SELECT word_count FROM {$this->wpdb->prefix}icl_string_packages WHERE post_id = %d",
$post_id
)
);
}
/**
* @param int $package_id
* @param string $word_count
*/
public function set_word_count( $package_id, $word_count ) {
$this->wpdb->update(
$this->wpdb->prefix . 'icl_string_packages',
array( 'word_count' => $word_count ),
array( 'ID' => $package_id )
);
}
/**
* @param int $package_id
*
* @return null|string
*/
public function get_word_count( $package_id ) {
return $this->wpdb->get_var(
$this->wpdb->prepare(
"SELECT word_count FROM {$this->wpdb->prefix}icl_string_packages WHERE ID = %d",
$package_id
)
);
}
public function reset_all( array $package_kinds ) {
if ( ! $package_kinds ) {
return;
}
$query = "UPDATE {$this->wpdb->prefix}icl_string_packages SET word_count = NULL
WHERE kind_slug IN(" . wpml_prepare_in( $package_kinds ) . ')';
$this->wpdb->query( $query );
}
/**
* @param array $kinds
*
* @return array
*/
public function get_ids_from_kind_slugs( array $kinds ) {
if ( ! $kinds ) {
return array();
}
$query = "SELECT ID FROM {$this->wpdb->prefix}icl_string_packages
WHERE kind_slug IN(" . wpml_prepare_in( $kinds ) . ')';
return array_map( 'intval', $this->wpdb->get_col( $query ) );
}
/**
* @param array $post_types
*
* @return array
*/
public function get_ids_from_post_types( array $post_types ) {
if ( ! $post_types ) {
return array();
}
$query = "SELECT sp.ID FROM {$this->wpdb->prefix}icl_string_packages AS sp
LEFT JOIN {$this->wpdb->posts} AS p
ON p.ID = sp.post_id
WHERE p.post_type IN(" . wpml_prepare_in( $post_types ) . ')';
return array_map( 'intval', $this->wpdb->get_col( $query ) );
}
/**
* @param string $kind_slug
*
* @return int
*/
public function count_items_by_kind_not_part_of_posts( $kind_slug ) {
$query = "SELECT COUNT(*) FROM {$this->wpdb->prefix}icl_string_packages
WHERE kind_slug = %s AND post_id IS NULL";
return (int) $this->wpdb->get_var( $this->wpdb->prepare( $query, $kind_slug ) );
}
/**
* @param string $kind_slug
*
* @return int
*/
public function count_word_counts_by_kind( $kind_slug ) {
$query = "SELECT COUNT(*) FROM {$this->wpdb->prefix}icl_string_packages
WHERE kind_slug = %s AND word_count IS NOT NULL
AND post_id IS NULL";
return (int) $this->wpdb->get_var( $this->wpdb->prepare( $query, $kind_slug ) );
}
/**
* @param string $kind_slug
*
* @return array
*/
public function get_word_counts_by_kind( $kind_slug ) {
$query = "SELECT word_count FROM {$this->wpdb->prefix}icl_string_packages
WHERE kind_slug = %s";
return $this->wpdb->get_col( $this->wpdb->prepare( $query, $kind_slug ) );
}
}

View File

@@ -0,0 +1,125 @@
<?php
class WPML_ST_Word_Count_String_Records {
const CACHE_GROUP = __CLASS__;
/** @var wpdb */
private $wpdb;
public function __construct( wpdb $wpdb ) {
$this->wpdb = $wpdb;
}
/** @return int */
public function get_total_words() {
return (int) $this->wpdb->get_var( "SELECT SUM(word_count) FROM {$this->wpdb->prefix}icl_strings" );
}
/** @return array */
public function get_all_values_without_word_count() {
$query = "
SELECT id, value FROM {$this->wpdb->prefix}icl_strings
WHERE word_count IS NULL
";
return $this->wpdb->get_results( $query );
}
/**
* @param string $lang
* @param null|string $package_id
*
* @return int
*/
public function get_words_to_translate_per_lang( $lang, $package_id = null ) {
$key = $lang . ':' . $package_id;
$found = false;
$words = WPML_Non_Persistent_Cache::get( $key, self::CACHE_GROUP, $found );
if ( ! $found ) {
$query = "
SELECT SUM(word_count) FROM {$this->wpdb->prefix}icl_strings AS s
LEFT JOIN {$this->wpdb->prefix}icl_string_translations AS st
ON st.string_id = s.id AND st.language = %s
WHERE (st.status <> %d OR st.status IS NULL)
";
$prepare_args = [
$lang,
ICL_STRING_TRANSLATION_COMPLETE,
];
if ( $package_id ) {
$query .= ' AND s.string_package_id = %d';
$prepare_args[] = $package_id;
}
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$words = (int) $this->wpdb->get_var( $this->wpdb->prepare( $query, $prepare_args ) );
WPML_Non_Persistent_Cache::set( $key, $words, self::CACHE_GROUP );
}
return $words;
}
/**
* @param int $string_id
*
* @return stdClass
*/
public function get_value_and_language( $string_id ) {
return $this->wpdb->get_row(
$this->wpdb->prepare(
"SELECT value, language FROM {$this->wpdb->prefix}icl_strings WHERE id = %d",
$string_id
)
);
}
/**
* @param int $string_id
* @param int $word_count
*/
public function set_word_count( $string_id, $word_count ) {
$this->wpdb->update(
$this->wpdb->prefix . 'icl_strings',
array( 'word_count' => $word_count ),
array( 'id' => $string_id )
);
}
/**
* @param int $string_id
*
* @return int
*/
public function get_word_count( $string_id ) {
return (int) $this->wpdb->get_var(
$this->wpdb->prepare(
"SELECT word_count FROM {$this->wpdb->prefix}icl_strings WHERE ID = %d",
$string_id
)
);
}
public function reset_all() {
$this->wpdb->query( "UPDATE {$this->wpdb->prefix}icl_strings SET word_count = NULL" );
}
/**
* @param array $package_ids
*
* @return array
*/
public function get_ids_from_package_ids( array $package_ids ) {
if ( ! $package_ids ) {
return array();
}
$query = "SELECT id FROM {$this->wpdb->prefix}icl_strings
WHERE string_package_id IN(" . wpml_prepare_in( $package_ids ) . ')';
return array_map( 'intval', $this->wpdb->get_col( $query ) );
}
}

View File

@@ -0,0 +1,108 @@
<?php
class WPML_Autoregister_Save_Strings {
const INSERT_CHUNK_SIZE = 200;
/**
* @var wpdb
*/
private $wpdb;
/**
* @var SitePress $sitepress
*/
private $sitepress;
/**
* @var array
*/
private $data = array();
/**
* @var WPML_Language_Of_Domain
*/
private $lang_of_domain;
/**
* @param wpdb $wpdb
* @param SitePress $sitepress
* @param WPML_Language_Of_Domain $language_of_domain
*/
public function __construct( wpdb $wpdb, SitePress $sitepress, WPML_Language_Of_Domain $language_of_domain = null ) {
$this->wpdb = $wpdb;
$this->sitepress = $sitepress;
$this->lang_of_domain = $language_of_domain ? $language_of_domain : new WPML_Language_Of_Domain( $this->sitepress );
add_action( 'shutdown', array( $this, 'shutdown' ) );
}
/**
* @param string $value
* @param string $name
* @param string $domain
* @param string $gettext_context
*/
public function save( $value, $name, $domain, $gettext_context = '' ) {
$this->data[] = array(
'value' => $value,
'name' => $name,
'domain' => $domain,
'gettext_context' => $gettext_context,
);
}
/**
* @param string $name
* @param string $domain
*
* @return string
*/
public function get_source_lang( $name, $domain ) {
$domain_lang = $this->lang_of_domain->get_language( $domain );
if ( ! $domain_lang ) {
$flag = 0 === strpos( $domain, 'admin_texts_' )
|| WPML_ST_Blog_Name_And_Description_Hooks::is_string( $name );
$domain_lang = $flag ? $this->sitepress->get_user_admin_language( get_current_user_id() ) : 'en';
}
return $domain_lang;
}
private function persist() {
foreach ( array_chunk( $this->data, self::INSERT_CHUNK_SIZE ) as $chunk ) {
$query = "INSERT IGNORE INTO {$this->wpdb->prefix}icl_strings "
. '(`language`, `context`, `gettext_context`, `domain_name_context_md5`, `name`, `value`, `status`) VALUES ';
$i = 0;
foreach ( $chunk as $string ) {
if ( $i > 0 ) {
$query .= ',';
}
$query .= $this->wpdb->prepare(
"('%s', '%s', '%s', '%s', '%s', '%s', %d)",
$this->get_source_lang( $string['name'], $string['domain'] ),
$string['domain'],
$string['gettext_context'],
md5( $string['domain'] . $string['name'] . $string['gettext_context'] ),
$string['name'],
$string['value'],
ICL_TM_NOT_TRANSLATED
);
$i ++;
}
$this->wpdb->query( $query );
}
}
public function shutdown() {
if ( count( $this->data ) ) {
$this->persist();
$this->data = array();
}
}
}

View File

@@ -0,0 +1,107 @@
<?php
class WPML_ST_Blog_Name_And_Description_Hooks implements \IWPML_Action {
const STRING_DOMAIN = 'WP';
const STRING_NAME_BLOGNAME = 'Blog Title';
const STRING_NAME_BLOGDESCRIPTION = 'Tagline';
/** @var array $cache */
private $cache = [];
/**
* Detect if ST is not installed on the current blog of multisite
*
* @var bool $is_active_on_current_blog
*/
private $is_active_on_current_blog = true;
public function add_hooks() {
if ( ! $this->is_customize_page() ) {
add_filter( 'option_blogname', [ $this, 'option_blogname_filter' ] );
add_filter( 'option_blogdescription', [ $this, 'option_blogdescription_filter' ] );
add_action( 'wpml_language_has_switched', [ $this, 'clear_cache' ] );
add_action( 'switch_blog', [ $this, 'switch_blog_action' ] );
}
}
/** @return bool */
private function is_customize_page() {
global $pagenow;
return 'customize.php' === $pagenow;
}
/**
* @param string $blogname
*
* @return string
*/
public function option_blogname_filter( $blogname ) {
return $this->translate_option( self::STRING_NAME_BLOGNAME, $blogname );
}
/**
* @param string $blogdescription
*
* @return string
*/
public function option_blogdescription_filter( $blogdescription ) {
return $this->translate_option( self::STRING_NAME_BLOGDESCRIPTION, $blogdescription );
}
/**
* @param string $name
* @param string $value
*
* @return string
*/
private function translate_option( $name, $value ) {
if ( ! $this->is_active_on_current_blog || ! wpml_st_is_requested_blog() ) {
return $value;
}
if ( ! isset( $this->cache[ $name ] ) ) {
$this->cache[ $name ] = wpml_get_string_current_translation(
$value,
self::STRING_DOMAIN,
$name
);
}
return $this->cache[ $name ];
}
/**
* As the translation depends on `WPML_String_Translation::get_current_string_language`,
* we added this clear cache callback on `wpml_language_has_switched` as done
* in `WPML_String_Translation::wpml_language_has_switched`.
*/
public function clear_cache() {
$this->cache = [];
}
public function switch_blog_action() {
$this->is_active_on_current_blog = is_plugin_active( basename( WPML_ST_PATH ) . '/plugin.php' );
}
/**
* @param string $string_name
*
* Checks whether a given string is to be translated in the Admin back-end.
* Currently only tagline and title of a site are to be translated.
* All other admin strings are to always be displayed in the user admin language.
*
* @return bool
*/
public static function is_string( $string_name ) {
return in_array(
$string_name,
[
self::STRING_NAME_BLOGDESCRIPTION,
self::STRING_NAME_BLOGNAME,
],
true
);
}
}

View File

@@ -0,0 +1,51 @@
<?php
class WPML_ST_Taxonomy_Labels_Translation_Factory implements IWPML_Backend_Action_Loader, IWPML_AJAX_Action_Loader {
const AJAX_ACTION_BUILD = 'wpml_get_terms_and_labels_for_taxonomy_table';
const AJAX_ACTION_SAVE = 'wpml_tt_save_labels_translation';
const AJAX_ACTION_CHANGE_LANG = 'wpml_tt_change_tax_strings_language';
const AJAX_ACTION_SET_SLUG_TRANSLATION_ENABLE = 'wpml_tt_set_slug_translation_enabled';
public function create() {
global $sitepress;
if ( $this->is_taxonomy_translation_table_action() ) {
$records_factory = new WPML_Slug_Translation_Records_Factory();
$taxonomy_strings = new WPML_ST_Taxonomy_Strings(
$records_factory->create( WPML_Slug_Translation_Factory::TAX ),
WPML\Container\make( WPML_ST_String_Factory::class )
);
$hooks[] = new WPML_ST_Taxonomy_Labels_Translation(
$taxonomy_strings,
new WPML_ST_Tax_Slug_Translation_Settings(),
new WPML_Super_Globals_Validation(),
$sitepress->get_active_languages( true )
);
if ( $this->is_wcml_active() ) {
$hooks[] = new WPML_ST_WCML_Taxonomy_Labels_Translation();
}
return $hooks;
}
return null;
}
private function is_taxonomy_translation_table_action() {
$allowed_actions = array(
self::AJAX_ACTION_BUILD,
self::AJAX_ACTION_SAVE,
self::AJAX_ACTION_CHANGE_LANG,
self::AJAX_ACTION_SET_SLUG_TRANSLATION_ENABLE,
);
return isset( $_POST['action'] )
&& in_array( $_POST['action'], $allowed_actions, true );
}
private function is_wcml_active() {
return is_plugin_active( 'woocommerce-multilingual/wpml-woocommerce.php' );
}
}

View File

@@ -0,0 +1,236 @@
<?php
class WPML_ST_Taxonomy_Labels_Translation implements IWPML_Action {
const NONCE_TAXONOMY_TRANSLATION = 'wpml_taxonomy_translation_nonce';
const PRIORITY_GET_LABEL = 10;
/** @var WPML_ST_Taxonomy_Strings $taxonomy_strings */
private $taxonomy_strings;
/** @var WPML_ST_Tax_Slug_Translation_Settings $slug_translation_settings */
private $slug_translation_settings;
/** @var WPML_Super_Globals_Validation $super_globals */
private $super_globals;
/** @var array $active_languages */
private $active_languages;
public function __construct(
WPML_ST_Taxonomy_Strings $taxonomy_strings,
WPML_ST_Tax_Slug_Translation_Settings $slug_translation_settings,
WPML_Super_Globals_Validation $super_globals,
array $active_languages
) {
$this->taxonomy_strings = $taxonomy_strings;
$this->slug_translation_settings = $slug_translation_settings;
$this->super_globals = $super_globals;
$this->active_languages = $active_languages;
}
public function add_hooks() {
add_filter( 'gettext_with_context', array( $this, 'block_translation_and_init_strings' ), PHP_INT_MAX, 4 );
add_filter( 'wpml_label_translation_data', array( $this, 'get_label_translations' ), self::PRIORITY_GET_LABEL, 2 );
add_action( 'wp_ajax_wpml_tt_save_labels_translation', array( $this, 'save_label_translations' ) );
add_action( 'wp_ajax_wpml_tt_change_tax_strings_language', array( $this, 'change_taxonomy_strings_language' ) );
}
/**
* @param string $translation
* @param string $text
* @param string $gettext_context
* @param string $domain
*
* @return mixed
*/
public function block_translation_and_init_strings( $translation, $text, $gettext_context, $domain ) {
if ( WPML_ST_Taxonomy_Strings::CONTEXT_GENERAL === $gettext_context
|| WPML_ST_Taxonomy_Strings::CONTEXT_SINGULAR === $gettext_context
) {
$this->taxonomy_strings->create_string_if_not_exist( $text, $gettext_context, $domain );
$this->taxonomy_strings->add_to_translated_with_gettext_context( $text, $domain );
// We need to return the original string here so the rest of
// the label translation UI works.
return $text;
}
return $translation;
}
/**
* @param false $false
* @param string $taxonomy
*
* @return array|null
*/
public function get_label_translations( $false, $taxonomy ) {
list( $general, $singular, $slug ) = $this->taxonomy_strings->get_taxonomy_strings( $taxonomy );
if ( ! $general || ! $singular || ! $slug ) {
return null;
}
$source_lang = $general->get_language();
$general_translations = $this->get_translations( $general );
$singular_translations = $this->get_translations( $singular );
$slug_translations = $this->get_translations( $slug );
$data = array(
'st_default_lang' => $source_lang,
);
foreach ( array_keys( $this->active_languages ) as $lang ) {
if ( $lang === $source_lang ) {
continue;
}
$data[ $lang ]['general'] = $this->get_translation_value( $lang, $general_translations );
$data[ $lang ]['singular'] = $this->get_translation_value( $lang, $singular_translations );
$data[ $lang ]['slug'] = $this->get_translation_value( $lang, $slug_translations );
$data[ $lang ] = array_filter( $data[ $lang ] );
}
$data[ $source_lang ] = array(
'general' => $general->get_value(),
'singular' => $singular->get_value(),
'slug' => $slug->get_value(),
'original' => true,
'globalSlugTranslationEnabled' => $this->slug_translation_settings->is_enabled(),
'showSlugTranslationField' => true,
);
return $data;
}
/**
* @param WPML_ST_String $string
*
* @return array
*/
private function get_translations( WPML_ST_String $string ) {
$translations = array();
foreach ( $string->get_translations() as $translation ) {
$translations[ $translation->language ] = $translation;
}
return $translations;
}
/**
* @param string $lang
* @param array $translations
*
* @return string|null
*/
private function get_translation_value( $lang, array $translations ) {
$value = null;
if ( isset( $translations[ $lang ] ) ) {
if ( $translations[ $lang ]->value ) {
$value = $translations[ $lang ]->value;
} elseif ( $translations[ $lang ]->mo_string ) {
$value = $translations[ $lang ]->mo_string;
}
}
return $value;
}
public function save_label_translations() {
if ( ! $this->check_nonce() ) {
return;
}
$general_translation = $this->get_string_var_from_post( 'plural' );
$singular_translation = $this->get_string_var_from_post( 'singular' );
$slug_translation = $this->get_string_var_from_post( 'slug' );
$taxonomy_name = $this->get_string_var_from_post( 'taxonomy' );
$language = $this->get_string_var_from_post( 'taxonomy_language_code' );
if ( $general_translation && $singular_translation && $taxonomy_name && $language ) {
list( $general, $singular, $slug ) = $this->taxonomy_strings->get_taxonomy_strings( $taxonomy_name );
if ( $general && $singular && $slug ) {
$general->set_translation( $language, $general_translation, ICL_STRING_TRANSLATION_COMPLETE );
$singular->set_translation( $language, $singular_translation, ICL_STRING_TRANSLATION_COMPLETE );
$slug->set_translation( $language, $slug_translation, ICL_STRING_TRANSLATION_COMPLETE );
$slug_translation_enabled = $this->has_slug_translation( $slug );
$this->slug_translation_settings->set_type( $taxonomy_name, $slug_translation_enabled );
$this->slug_translation_settings->save();
$result = array(
'general' => $general_translation,
'singular' => $singular_translation,
'slug' => $slug_translation,
'lang' => $language,
);
wp_send_json_success( $result );
return;
}
}
wp_send_json_error();
}
private function has_slug_translation( WPML_ST_String $slug ) {
$translations = $slug->get_translations();
if ( $translations ) {
foreach ( $translations as $translation ) {
if ( trim( $translation->value ) && ICL_STRING_TRANSLATION_COMPLETE === (int) $translation->status ) {
return true;
}
}
}
return false;
}
public function change_taxonomy_strings_language() {
if ( ! $this->check_nonce() ) {
return;
}
$taxonomy = $this->get_string_var_from_post( 'taxonomy' );
$source_lang = $this->get_string_var_from_post( 'source_lang' );
if ( ! $taxonomy || ! $source_lang ) {
wp_send_json_error( __( 'Missing parameters', 'wpml-string-translation' ) );
return;
}
list( $general_string, $singular_string, $slug ) = $this->taxonomy_strings->get_taxonomy_strings( $taxonomy );
$general_string->set_language( $source_lang );
$singular_string->set_language( $source_lang );
$slug->set_language( $source_lang );
wp_send_json_success();
}
/**
* @param string $key
*
* @return false|string
*/
private function get_string_var_from_post( $key ) {
$value = $this->super_globals->post( $key );
return null !== $value ? sanitize_text_field( $value ) : false;
}
private function check_nonce() {
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], self::NONCE_TAXONOMY_TRANSLATION ) ) {
wp_send_json_error( __( 'Invalid nonce', 'wpml-string-translation' ) );
return false;
}
return true;
}
}

View File

@@ -0,0 +1,49 @@
<?php
class WPML_ST_WCML_Taxonomy_Labels_Translation implements IWPML_Action {
public function add_hooks() {
add_filter(
'wpml_label_translation_data',
array( $this, 'alter_slug_translation_display' ),
WPML_ST_Taxonomy_Labels_Translation::PRIORITY_GET_LABEL + 1,
2
);
}
/**
* @param array $data
* @param string $taxonomy
*
* @return array
*/
public function alter_slug_translation_display( $data, $taxonomy ) {
if ( ! empty( $data['st_default_lang'] ) ) {
$source_lang = $data['st_default_lang'];
if ( $this->is_product_attribute( $taxonomy ) || $this->is_shipping_class( $taxonomy ) ) {
$data[ $source_lang ]['showSlugTranslationField'] = false;
}
}
return $data;
}
/**
* @param string $taxonomy
*
* @return bool
*/
private function is_product_attribute( $taxonomy ) {
return 0 === strpos( $taxonomy, 'pa_' );
}
/**
* @param string $taxonomy
*
* @return bool
*/
private function is_shipping_class( $taxonomy ) {
return 'product_shipping_class' === $taxonomy;
}
}

View File

@@ -0,0 +1,71 @@
<?php
class WPML_TM_Filters {
/** @var array */
private $string_lang_codes;
/** @var wpdb */
private $wpdb;
/** @var SitePress */
private $sitepress;
/**
* WPML_TM_Filters constructor.
*
* @param wpdb $wpdb
* @param SitePress $sitepress
*/
public function __construct( wpdb $wpdb, SitePress $sitepress ) {
$this->wpdb = $wpdb;
$this->sitepress = $sitepress;
}
/**
* Filters the active languages to include all languages in which strings exist.
*
* @param WPML_Language_Collection $source_langs
*
* @return array[]
*/
public function filter_tm_source_langs( WPML_Language_Collection $source_langs ) {
foreach ( $this->get_string_lang_codes() as $lang_code ) {
$source_langs->add( $lang_code );
}
return $source_langs;
}
private function get_string_lang_codes() {
if ( null === $this->string_lang_codes ) {
$this->string_lang_codes = $this->wpdb->get_col( "SELECT DISTINCT(s.language) FROM {$this->wpdb->prefix}icl_strings s" );
}
return $this->string_lang_codes;
}
/**
* This filters the check whether or not a job is assigned to a specific translator for local string jobs.
* It is to be used after assigning a job, as it will update the assignment for local string jobs itself.
*
* @param bool $assigned_correctly
* @param string|int $string_translation_id
* @param int $translator_id
* @param string|int $service
*
* @return bool
*/
public function job_assigned_to_filter( $assigned_correctly, $string_translation_id, $translator_id, $service ) {
if ( ( ! $service || $service === 'local' ) && strpos( $string_translation_id, 'string|' ) !== false ) {
$string_translation_id = preg_replace( '/[^0-9]/', '', $string_translation_id );
$this->wpdb->update(
$this->wpdb->prefix . 'icl_string_translations',
array( 'translator_id' => $translator_id ),
array( 'id' => $string_translation_id )
);
$assigned_correctly = true;
}
return $assigned_correctly;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace WPML\ST\StringsFilter;
use WPML_Displayed_String_Filter;
use WPML_Register_String_Filter;
use WPML_String_Translation;
class Provider {
/** @var WPML_String_Translation */
private $string_translation;
/** @var WPML_Displayed_String_Filter[]|WPML_Register_String_Filter[] */
private $filters = [];
public function __construct( WPML_String_Translation $string_translation ) {
$this->string_translation = $string_translation;
}
/**
* Get filter.
*
* @param string|null $lang Language.
* @param string|null $name Language name.
*
* @return WPML_Displayed_String_Filter|WPML_Register_String_Filter|null
*/
public function getFilter( $lang = null, $name = null ) {
if ( ! $lang ) {
$lang = $this->string_translation->get_current_string_language( $name );
}
if ( ! $lang ) {
return null;
}
if ( ! ( array_key_exists( $lang, $this->filters ) && $this->filters[ $lang ] ) ) {
$this->filters[ $lang ] = $this->string_translation->get_string_filter( $lang );
}
return $this->filters[ $lang ];
}
public function clearFilters() {
$this->filters = [];
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace WPML\ST\StringsFilter;
class QueryBuilder {
/** @var \wpdb */
private $wpdb;
/** @var string|null $language */
private $language;
/** @var string */
private $where;
public function __construct( \wpdb $wpdb ) {
$this->wpdb = $wpdb;
}
/**
* @param string $language
*
* @return $this
*/
public function setLanguage( $language ) {
$this->language = $language;
return $this;
}
/**
* @param array $domains
*
* @return $this
*/
public function filterByDomains( array $domains ) {
$in = \wpml_prepare_in( $domains );
$this->where = "s.context IN({$in})";
return $this;
}
/**
* @param StringEntity $string
*
* @return $this
*/
public function filterByString( StringEntity $string ) {
$this->where = $this->wpdb->prepare(
's.name = %s AND s.context = %s AND s.gettext_context = %s',
$string->getName(),
$string->getDomain(),
$string->getContext()
);
return $this;
}
/**
* @return string
*/
public function build() {
$result = $this->getSQL();
if ( $this->where ) {
$result .= ' WHERE ' . $this->where;
}
return $result;
}
/**
* @return string
*/
private function getSQL() {
return $this->wpdb->prepare(
"
SELECT
s.value,
s.name,
s.context as domain,
s.gettext_context as context,
IF(st.status = %d AND st.value IS NOT NULL, st.`value`, st.mo_string) AS `translation`
FROM {$this->wpdb->prefix}icl_strings s
LEFT JOIN {$this->wpdb->prefix}icl_string_translations st ON st.string_id = s.id AND st.`language` = %s
",
ICL_STRING_TRANSLATION_COMPLETE,
$this->language
);
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace WPML\ST\StringsFilter;
class StringEntity {
/** @var string */
private $value;
/** @var string */
private $name;
/** @var string */
private $domain;
/** @var string */
private $context;
/**
* @param string $value
* @param string $name
* @param string $domain
* @param string $context
*/
public function __construct( $value, $name, $domain, $context = '' ) {
$this->value = $value;
$this->name = $name;
$this->domain = $domain;
$this->context = $context;
}
/**
* @return string
*/
public function getValue() {
return $this->value;
}
/**
* @return string
*/
public function getName() {
return $this->name;
}
/**
* @return string
*/
public function getDomain() {
return $this->domain;
}
/**
* @return string
*/
public function getContext() {
return $this->context;
}
/**
* @param array $data
*
* @return StringEntity
*/
public static function fromArray( array $data ) {
return new self( $data['value'], $data['name'], $data['domain'], $data['context'] );
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace WPML\ST\StringsFilter;
class TranslationEntity {
/** @var string */
private $value;
/** @var bool */
private $hasTranslation;
/** @var bool */
private $stringRegistered;
/**
* @param string $value
* @param bool $hasTranslation
* @param bool $stringRegistered
*/
public function __construct( $value, $hasTranslation, $stringRegistered = true ) {
$this->value = $value;
$this->hasTranslation = $hasTranslation;
$this->stringRegistered = $stringRegistered;
}
/**
* @return string
*/
public function getValue() {
return $this->value;
}
/**
* @return bool
*/
public function isStringRegistered() {
return $this->stringRegistered;
}
/**
* @return bool
*/
public function hasTranslation() {
return $this->hasTranslation;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace WPML\ST\StringsFilter;
class TranslationReceiver {
/** @var \wpdb */
private $wpdb;
/** @var QueryBuilder $query_builder */
private $query_builder;
public function __construct( \wpdb $wpdb, QueryBuilder $query_builder ) {
$this->wpdb = $wpdb;
$this->query_builder = $query_builder;
}
/**
* @param StringEntity $string
* @param string $language
*
* @return TranslationEntity
*/
public function get( StringEntity $string, $language ) {
$query = $this->query_builder->setLanguage( $language )->filterByString( $string )->build();
$record = $this->wpdb->get_row( $query, ARRAY_A );
if ( ! $record ) {
return new TranslationEntity( $string->getValue(), false, false );
}
if ( ! $record['translation'] ) {
return new TranslationEntity( $record['value'], false, true );
}
return new TranslationEntity( $record['translation'], true, true );
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace WPML\ST\StringsFilter;
class Translations {
/** @var \SplObjectStorage */
private $data;
public function __construct() {
$this->data = new TranslationsObjectStorage();
}
/**
* @param StringEntity $string
* @param TranslationEntity $translation
*/
public function add( StringEntity $string, TranslationEntity $translation ) {
$this->data->attach( $string, $translation );
}
/**
* @param StringEntity $string
*
* @return TranslationEntity|null
*/
public function get( StringEntity $string ) {
return $this->data->contains( $string ) ? $this->data[ $string ] : null;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace WPML\ST\StringsFilter;
use WPML\ST\StringsFilter\StringEntity;
/**
* This storage in used internally in "Translations" class. Unfortunately, I cannot use anonymous classes due to PHP Version limitation.
*/
class TranslationsObjectStorage extends \SplObjectStorage {
/**
* @param StringEntity $o
*
* @return string
*/
#[\ReturnTypeWillChange]
public function getHash( $o ) {
return implode(
'_',
[
$o->getValue(),
$o->getName(),
$o->getDomain(),
$o->getContext(),
]
);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace WPML\ST\StringsFilter;
class Translator {
/** @var string */
private $language;
/** @var TranslationReceiver */
private $translationReceiver;
/** @var Translations */
private $translations;
/**
* @param string $language
* @param TranslationReceiver $translationReceiver
*/
public function __construct(
$language,
TranslationReceiver $translationReceiver
) {
$this->language = $language;
$this->translationReceiver = $translationReceiver;
}
/**
* @param StringEntity $string
*
* @return TranslationEntity
*/
public function translate( StringEntity $string ) {
if ( $this->translations === null ) {
$this->translations = new Translations();
}
$translation = $this->translations->get( $string );
if ( ! $translation ) {
$translation = $this->translationReceiver->get( $string, $this->language );
$this->translations->add( $string, $translation );
}
return $translation;
}
}

View File

@@ -0,0 +1,119 @@
<?php
/**
* WPML_Displayed_String_Filter class file.
*
* @package WPML\ST
*/
use WPML\ST\StringsFilter\Translator;
use WPML\ST\StringsFilter\StringEntity;
use WPML\ST\StringsFilter\TranslationEntity;
/**
* Class WPML_Displayed_String_Filter
*
* Handles all string translating when rendering translated strings to the user, unless auto-registering is
* active for strings.
*/
class WPML_Displayed_String_Filter {
/** @var Translator */
protected $translator;
/**
* @param Translator $translator
*/
public function __construct( Translator $translator ) {
$this->translator = $translator;
}
/**
* Translate by name and context.
*
* @param string $untranslated_text Untranslated text.
* @param string $name Name of the string.
* @param string|array $context Context.
* @param null|boolean $has_translation If string has translation.
*
* @return string
*/
public function translate_by_name_and_context(
$untranslated_text,
$name,
$context = '',
&$has_translation = null
) {
if ( is_array( $untranslated_text ) || is_object( $untranslated_text ) ) {
return '';
}
$translation = $this->get_translation( $untranslated_text, $name, $context );
$has_translation = $translation->hasTranslation();
return $translation->getValue();
}
/**
* Transform translation parameters.
*
* @param string $name Name of the string.
* @param string|array $context Context.
*
* @return array
*/
protected function transform_parameters( $name, $context ) {
list ( $domain, $gettext_context ) = wpml_st_extract_context_parameters( $context );
return array( $name, $domain, $gettext_context );
}
/**
* Truncates a string to the maximum string table column width.
*
* @param string $string String to translate.
*
* @return string
*/
public static function truncate_long_string( $string ) {
return strlen( $string ) > WPML_STRING_TABLE_NAME_CONTEXT_LENGTH
? mb_substr( $string, 0, WPML_STRING_TABLE_NAME_CONTEXT_LENGTH )
: $string;
}
/**
* Get translation of the string.
*
* @param string $untranslated_text Untranslated text.
* @param string $name Name of the string.
* @param string|array $context Context.
*
* @return TranslationEntity
*/
protected function get_translation( $untranslated_text, $name, $context ) {
list ( $name, $domain, $gettext_context ) = $this->transform_parameters( $name, $context );
$untranslated_text = is_numeric( $untranslated_text ) ? (string) $untranslated_text : $untranslated_text;
$translation = $this->translator->translate(
new StringEntity(
$untranslated_text,
$name,
$domain,
$gettext_context
)
);
if ( ! $translation->hasTranslation() ) {
list( $name, $domain ) = array_map( array( $this, 'truncate_long_string' ), array( $name, $domain ) );
$translation = $this->translator->translate(
new StringEntity(
$untranslated_text,
$name,
$domain,
$gettext_context
)
);
}
return $translation;
}
}

Some files were not shown because too many files have changed in this diff Show More