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,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;
}
}