first commit
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace WPML\ST\TranslationFile;
|
||||
|
||||
abstract class Builder {
|
||||
/** @var string $plural_form */
|
||||
protected $plural_form = 'nplurals=2; plural=n != 1;';
|
||||
/** @var string $language */
|
||||
protected $language;
|
||||
|
||||
/**
|
||||
* @param string $language
|
||||
*
|
||||
* @return Builder
|
||||
*/
|
||||
public function set_language( $language ) {
|
||||
$this->language = $language;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $plural_form
|
||||
*
|
||||
* @return Builder
|
||||
*/
|
||||
public function set_plural_form( $plural_form ) {
|
||||
$this->plural_form = $plural_form;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StringEntity[] $strings
|
||||
* @return string
|
||||
*/
|
||||
abstract public function get_content( array $strings);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace WPML\ST\TranslationFile;
|
||||
|
||||
use wpdb;
|
||||
use WPML\Collect\Support\Collection;
|
||||
use WPML\ST\Package\Domains as PackageDomains;
|
||||
use WPML_Admin_Texts;
|
||||
use function wpml_prepare_in;
|
||||
use WPML_Slug_Translation;
|
||||
use WPML_ST_Blog_Name_And_Description_Hooks;
|
||||
use WPML_ST_Translations_File_Dictionary;
|
||||
use WPML\ST\Shortcode;
|
||||
|
||||
class Domains {
|
||||
|
||||
/** @var wpdb $wpdb */
|
||||
private $wpdb;
|
||||
|
||||
/** @var PackageDomains $package_domains */
|
||||
private $package_domains;
|
||||
|
||||
/** @var WPML_ST_Translations_File_Dictionary $file_dictionary */
|
||||
private $file_dictionary;
|
||||
|
||||
/** @var null|Collection $mo_domains */
|
||||
private static $mo_domains;
|
||||
|
||||
/** @var null|Collection $jed_domains */
|
||||
private static $jed_domains;
|
||||
|
||||
/**
|
||||
* Domains constructor.
|
||||
*
|
||||
* @param PackageDomains $package_domains
|
||||
* @param WPML_ST_Translations_File_Dictionary $file_dictionary
|
||||
*/
|
||||
public function __construct(
|
||||
wpdb $wpdb,
|
||||
PackageDomains $package_domains,
|
||||
WPML_ST_Translations_File_Dictionary $file_dictionary
|
||||
) {
|
||||
$this->wpdb = $wpdb;
|
||||
$this->package_domains = $package_domains;
|
||||
$this->file_dictionary = $file_dictionary;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
public function getMODomains() {
|
||||
if ( ! self::$mo_domains instanceof Collection ) {
|
||||
$excluded_domains = self::getReservedDomains()
|
||||
->merge( $this->getJEDDomains() );
|
||||
|
||||
$sql = "
|
||||
SELECT DISTINCT context {$this->getCollateForContextColumn()}
|
||||
FROM {$this->wpdb->prefix}icl_strings
|
||||
";
|
||||
|
||||
self::$mo_domains = wpml_collect( $this->wpdb->get_col( $sql ) )
|
||||
->diff( $excluded_domains )
|
||||
->values();
|
||||
|
||||
}
|
||||
|
||||
return self::$mo_domains;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function getCollateForContextColumn() {
|
||||
$sql = "
|
||||
SELECT COLLATION_NAME
|
||||
FROM information_schema.columns
|
||||
WHERE TABLE_SCHEMA = '" . DB_NAME . "' AND TABLE_NAME = '{$this->wpdb->prefix}icl_strings' AND COLUMN_NAME = 'context'
|
||||
";
|
||||
|
||||
$collation = $this->wpdb->get_var( $sql );
|
||||
if ( ! $collation ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
list( $type ) = explode( '_', $collation );
|
||||
if ( in_array( $type, [ 'utf8', 'utf8mb4' ] ) ) {
|
||||
return 'COLLATE ' . $type . '_bin';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection of MO domains that
|
||||
* WPML needs to automatically load.
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getCustomMODomains() {
|
||||
$all_mo_domains = $this->getMODomains();
|
||||
$native_mo_domains = $this->file_dictionary->get_domains( 'mo', get_locale() );
|
||||
|
||||
return $all_mo_domains->reject(
|
||||
function( $domain ) use ( $native_mo_domains ) {
|
||||
/**
|
||||
* Admin texts, packages, string shortcodes are handled separately,
|
||||
* so they are loaded on-demand.
|
||||
*/
|
||||
return null === $domain
|
||||
|| 0 === strpos( $domain, WPML_Admin_Texts::DOMAIN_NAME_PREFIX )
|
||||
|| $this->package_domains->isPackage( $domain )
|
||||
|| Shortcode::STRING_DOMAIN === $domain
|
||||
|| in_array( $domain, $native_mo_domains, true );
|
||||
}
|
||||
)->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
public function getJEDDomains() {
|
||||
if ( ! self::$jed_domains instanceof Collection ) {
|
||||
self::$jed_domains = wpml_collect( $this->file_dictionary->get_domains( 'json' ) );
|
||||
}
|
||||
|
||||
return self::$jed_domains;
|
||||
}
|
||||
|
||||
public static function resetCache() {
|
||||
self::$mo_domains = null;
|
||||
self::$jed_domains = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Domains that are not handled with MO files,
|
||||
* but have direct DB queries.
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public static function getReservedDomains() {
|
||||
return wpml_collect(
|
||||
[
|
||||
WPML_ST_Blog_Name_And_Description_Hooks::STRING_DOMAIN,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
private function getPackageDomains() {
|
||||
return $this->package_domains->getDomains();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace WPML\ST\TranslationFile;
|
||||
|
||||
use wpdb;
|
||||
use WPML\Collect\Support\Collection;
|
||||
use WPML\FP\Fns;
|
||||
use WPML\FP\Lst;
|
||||
use WPML\FP\Obj;
|
||||
use WPML_Locale;
|
||||
|
||||
class DomainsLocalesMapper {
|
||||
|
||||
const ALIAS_STRINGS = 's';
|
||||
const ALIAS_STRING_TRANSLATIONS = 'st';
|
||||
|
||||
/** @var wpdb $wpdb */
|
||||
private $wpdb;
|
||||
|
||||
/** @var WPML_Locale $locale */
|
||||
private $locale;
|
||||
|
||||
public function __construct( wpdb $wpdb, WPML_Locale $locale ) {
|
||||
$this->wpdb = $wpdb;
|
||||
$this->locale = $locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $string_translation_ids
|
||||
*
|
||||
* @return Collection of objects with properties `domain` and `locale`
|
||||
*/
|
||||
public function get_from_translation_ids( array $string_translation_ids ) {
|
||||
return $this->get_results_where( self::ALIAS_STRING_TRANSLATIONS, $string_translation_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $string_ids
|
||||
*
|
||||
* @return Collection of objects with properties `domain` and `locale`
|
||||
*/
|
||||
public function get_from_string_ids( array $string_ids ) {
|
||||
return $this->get_results_where( self::ALIAS_STRINGS, $string_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $getActiveLanguages
|
||||
* @param string $domain
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_from_domain( callable $getActiveLanguages, $domain ) {
|
||||
$createEntity = function ( $locale ) use ( $domain ) {
|
||||
return (object) [
|
||||
'domain' => $domain,
|
||||
'locale' => $locale,
|
||||
];
|
||||
};
|
||||
|
||||
return Fns::map( $createEntity, Obj::values( Lst::pluck( 'default_locale', $getActiveLanguages() ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $table_alias
|
||||
* @param array $ids
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
private function get_results_where( $table_alias, array $ids ) {
|
||||
$results = [];
|
||||
if ( array_filter( $ids ) ) {
|
||||
$results = $this->wpdb->get_results(
|
||||
"
|
||||
SELECT DISTINCT
|
||||
s.context AS domain,
|
||||
st.language
|
||||
FROM {$this->wpdb->prefix}icl_string_translations AS " . self::ALIAS_STRING_TRANSLATIONS . "
|
||||
JOIN {$this->wpdb->prefix}icl_strings AS " . self::ALIAS_STRINGS . " ON s.id = st.string_id
|
||||
WHERE $table_alias.id IN(" . wpml_prepare_in( $ids ) . ')
|
||||
'
|
||||
);
|
||||
}
|
||||
|
||||
return wpml_collect( $results )->map(
|
||||
function( $row ) {
|
||||
return (object) [
|
||||
'domain' => $row->domain,
|
||||
'locale' => $this->locale->get_locale( $row->language ),
|
||||
];
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace WPML\ST\TranslationFile;
|
||||
|
||||
use WPML\ST\MO\Hooks\Factory;
|
||||
use WPML\ST\MO\Plural;
|
||||
use WPML_Action_Filter_Loader;
|
||||
use WPML_Package_Translation_Schema;
|
||||
use WPML_ST_Upgrade;
|
||||
use WPML_ST_Upgrade_MO_Scanning;
|
||||
|
||||
class Hooks {
|
||||
|
||||
/** @var WPML_Action_Filter_Loader $action_loader */
|
||||
private $action_loader;
|
||||
|
||||
/** @var WPML_ST_Upgrade $upgrade */
|
||||
private $upgrade;
|
||||
|
||||
public function __construct( WPML_Action_Filter_Loader $action_loader, WPML_ST_Upgrade $upgrade ) {
|
||||
$this->action_loader = $action_loader;
|
||||
$this->upgrade = $upgrade;
|
||||
}
|
||||
|
||||
public function install() {
|
||||
if ( $this->hasPackagesTable() && $this->hasTranslationFilesTables() ) {
|
||||
$this->action_loader->load( [ Factory::class, Plural::class ] );
|
||||
}
|
||||
}
|
||||
|
||||
private function hasPackagesTable() {
|
||||
$updates_run = get_option( WPML_Package_Translation_Schema::OPTION_NAME, [] );
|
||||
return in_array( WPML_Package_Translation_Schema::REQUIRED_VERSION, $updates_run, true );
|
||||
}
|
||||
|
||||
private function hasTranslationFilesTables() {
|
||||
return $this->upgrade->has_command_been_executed( WPML_ST_Upgrade_MO_Scanning::class );
|
||||
}
|
||||
|
||||
public static function useFileSynchronization() {
|
||||
return defined( 'WPML_ST_SYNC_TRANSLATION_FILES' ) && constant( 'WPML_ST_SYNC_TRANSLATION_FILES' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace WPML\ST\TranslationFile;
|
||||
|
||||
use WP_Filesystem_Direct;
|
||||
use WPML\Collect\Support\Collection;
|
||||
use WPML\ST\MO\File\makeDir;
|
||||
use WPML_Language_Records;
|
||||
use WPML_ST_Translations_File_Dictionary;
|
||||
use function wpml_collect;
|
||||
use WPML_ST_Translations_File_Entry;
|
||||
|
||||
abstract class Manager {
|
||||
|
||||
use makeDir;
|
||||
|
||||
const SUB_DIRECTORY = 'wpml';
|
||||
|
||||
/** @var StringsRetrieve $strings */
|
||||
protected $strings;
|
||||
/** @var WPML_Language_Records $language_records */
|
||||
protected $language_records;
|
||||
/** @var Builder $builder */
|
||||
protected $builder;
|
||||
/** @var WPML_ST_Translations_File_Dictionary $file_dictionary */
|
||||
protected $file_dictionary;
|
||||
/** @var Domains $domains */
|
||||
protected $domains;
|
||||
|
||||
public function __construct(
|
||||
StringsRetrieve $strings,
|
||||
Builder $builder,
|
||||
WP_Filesystem_Direct $filesystem,
|
||||
WPML_Language_Records $language_records,
|
||||
Domains $domains
|
||||
) {
|
||||
$this->strings = $strings;
|
||||
$this->builder = $builder;
|
||||
$this->filesystem = $filesystem;
|
||||
$this->language_records = $language_records;
|
||||
$this->domains = $domains;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $domain
|
||||
* @param string $locale
|
||||
*/
|
||||
public function remove( $domain, $locale ) {
|
||||
$this->filesystem->delete( $this->getFilepath( $domain, $locale ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $domain
|
||||
* @param string $locale
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function add( $domain, $locale ) {
|
||||
if ( ! $this->maybeCreateSubdir() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lang_code = $this->language_records->get_language_code( $locale );
|
||||
$strings = $this->strings->get( $domain, $lang_code, $this->isPartialFile() );
|
||||
|
||||
if ( ! $strings && $this->isPartialFile() ) {
|
||||
$this->remove( $domain, $locale );
|
||||
return false;
|
||||
}
|
||||
|
||||
$file_content = $this->builder
|
||||
->set_language( $locale )
|
||||
->get_content( $strings );
|
||||
|
||||
$filepath = $this->getFilepath( $domain, $locale );
|
||||
|
||||
return $this->filesystem->put_contents( $filepath, $file_content, 0755 & ~ umask() );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $domain
|
||||
* @param string $locale
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function get( $domain, $locale ) {
|
||||
$filepath = $this->getFilepath( $domain, $locale );
|
||||
|
||||
if ( $this->filesystem->is_file( $filepath ) && $this->filesystem->is_readable( $filepath ) ) {
|
||||
return $filepath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $domain
|
||||
* @param string $locale
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFilepath( $domain, $locale ) {
|
||||
return $this->getSubdir() . '/' . strtolower( $domain ) . '-' . $locale . '.' . $this->getFileExtension();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $domain
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function handles( $domain ) {
|
||||
return $this->getDomains()->contains( $domain );
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public static function getSubdir() {
|
||||
$subdir = WP_LANG_DIR . '/' . self::SUB_DIRECTORY;
|
||||
|
||||
$siteId = get_current_blog_id();
|
||||
if ( $siteId > 1 ) {
|
||||
$subdir .= '/' . $siteId;
|
||||
}
|
||||
|
||||
return $subdir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function getFileExtension();
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function isPartialFile();
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
abstract protected function getDomains();
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace WPML\ST\TranslationFile;
|
||||
|
||||
class StringEntity {
|
||||
|
||||
/** @var string $original */
|
||||
private $original;
|
||||
|
||||
/** @var array $translations */
|
||||
private $translations = array();
|
||||
|
||||
/** @var null|string $context */
|
||||
private $context;
|
||||
|
||||
/** @var string|null */
|
||||
private $original_plural;
|
||||
|
||||
/** @var string|null */
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @param string $original
|
||||
* @param array $translations
|
||||
* @param null|string $context
|
||||
* @param null|string $original_plural
|
||||
* @param null|string $name
|
||||
*/
|
||||
public function __construct( $original, array $translations, $context = null, $original_plural = null, $name = null ) {
|
||||
$this->original = $original;
|
||||
$this->translations = $translations;
|
||||
$this->context = $context ? $context : null;
|
||||
$this->original_plural = $original_plural;
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function get_original() {
|
||||
return $this->original;
|
||||
}
|
||||
|
||||
/** @return array */
|
||||
public function get_translations() {
|
||||
return $this->translations;
|
||||
}
|
||||
|
||||
/** @return null|string */
|
||||
public function get_context() {
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_original_plural() {
|
||||
return $this->original_plural;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_name() {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*/
|
||||
public function set_name( $name ) {
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace WPML\ST\TranslationFile;
|
||||
|
||||
use WPML\Collect\Support\Collection;
|
||||
use WPML\ST\TranslateWpmlString;
|
||||
|
||||
class StringsRetrieve {
|
||||
|
||||
// We need to store the strings by key that is a combination of original and gettext context
|
||||
// The join needs to be something that is unlikely to be in either so we can split later.
|
||||
const KEY_JOIN = '::JOIN::';
|
||||
|
||||
/** @var \WPML\ST\DB\Mappers\StringsRetrieve */
|
||||
private $string_retrieve;
|
||||
|
||||
/**
|
||||
* @param \WPML\ST\DB\Mappers\StringsRetrieve $string_retrieve
|
||||
*/
|
||||
public function __construct( \WPML\ST\DB\Mappers\StringsRetrieve $string_retrieve ) {
|
||||
$this->string_retrieve = $string_retrieve;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $domain
|
||||
* @param string $language
|
||||
* @param bool $modified_mo_only
|
||||
*
|
||||
* @return StringEntity[]
|
||||
*/
|
||||
public function get( $domain, $language, $modified_mo_only ) {
|
||||
return $this->loadFromDb( $language, $domain, $modified_mo_only )
|
||||
->filter(
|
||||
function ( $string ) {
|
||||
return (bool) $string['translation'];
|
||||
}
|
||||
)
|
||||
->mapToGroups(
|
||||
function ( array $string ) {
|
||||
return $this->groupPluralFormsOfSameString( $string );
|
||||
}
|
||||
)
|
||||
->map(
|
||||
function ( Collection $strings, $key ) {
|
||||
return $this->buildStringEntity( $strings, $key );
|
||||
}
|
||||
)
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $language
|
||||
* @param string $domain
|
||||
* @param bool $modified_mo_only
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
private function loadFromDb( $language, $domain, $modified_mo_only = false ) {
|
||||
$result = \wpml_collect( $this->string_retrieve->get( $language, $domain, $modified_mo_only ) );
|
||||
|
||||
return $result->map(
|
||||
function ( $row ) {
|
||||
return $this->parseResult( $row );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $row_data
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function parseResult( array $row_data ) {
|
||||
return [
|
||||
'id' => $row_data['id'],
|
||||
'original' => $row_data['original'],
|
||||
'context' => $row_data['gettext_context'],
|
||||
'translation' => self::parseTranslation( $row_data ),
|
||||
'name' => $row_data['name'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $row_data
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public static function parseTranslation( array $row_data ) {
|
||||
$value = null;
|
||||
|
||||
$has_translation = ! empty( $row_data['translated'] ) && in_array( $row_data['status'], [ ICL_TM_COMPLETE, ICL_TM_NEEDS_UPDATE ] );
|
||||
if ( $has_translation ) {
|
||||
$value = $row_data['translated'];
|
||||
} elseif ( ! empty( $row_data['mo_string'] ) ) {
|
||||
$value = $row_data['mo_string'];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $string
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function groupPluralFormsOfSameString( array $string ) {
|
||||
$groupKey = $this->getPluralGroupKey( $string );
|
||||
$pattern = '/^(.+) \[plural ([0-9]+)\]$/';
|
||||
|
||||
if ( preg_match( $pattern, $string['original'], $matches ) ) {
|
||||
$string['original'] = $matches[1];
|
||||
$string['index'] = $matches[2];
|
||||
} else {
|
||||
$string['index'] = null;
|
||||
}
|
||||
|
||||
return [
|
||||
$string['original'] . self::KEY_JOIN . $string['context'] . self::KEY_JOIN . $groupKey => $string,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Inside a domain, we can have several occurrences of strings
|
||||
* with the same original, but with different names.
|
||||
* In this situation, we should not try to group plurals.
|
||||
*
|
||||
* @param array $string
|
||||
*
|
||||
* @return mixed|string
|
||||
*/
|
||||
private function getPluralGroupKey( array $string ) {
|
||||
$cannotBelongToPluralGroup = TranslateWpmlString::canTranslateWithMO( $string['original'], $string['name'] );
|
||||
|
||||
if ( $cannotBelongToPluralGroup ) {
|
||||
return $string['name'];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $strings
|
||||
* @param string $key
|
||||
*
|
||||
* @return StringEntity
|
||||
*/
|
||||
private function buildStringEntity( Collection $strings, $key ) {
|
||||
$translations = $strings->sortBy( 'index' )->pluck( 'translation' )->toArray();
|
||||
list( $original, $context ) = explode( self::KEY_JOIN, $key );
|
||||
$stringEntity = new StringEntity( $original, $translations, $context );
|
||||
$stringEntity->set_name( $strings->first()['name'] );
|
||||
|
||||
return $stringEntity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace WPML\ST\TranslationFile\Sync;
|
||||
|
||||
use WPML\ST\TranslationFile\Manager;
|
||||
use WPML_ST_Translations_File_Locale;
|
||||
|
||||
class FileSync {
|
||||
|
||||
/** @var Manager */
|
||||
private $manager;
|
||||
|
||||
/** @var TranslationUpdates */
|
||||
private $translationUpdates;
|
||||
|
||||
/** @var WPML_ST_Translations_File_Locale */
|
||||
private $fileLocale;
|
||||
|
||||
public function __construct(
|
||||
Manager $manager,
|
||||
TranslationUpdates $translationUpdates,
|
||||
WPML_ST_Translations_File_Locale $FileLocale
|
||||
) {
|
||||
$this->manager = $manager;
|
||||
$this->translationUpdates = $translationUpdates;
|
||||
$this->fileLocale = $FileLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Before to load the custom translation file, we'll:
|
||||
* - Re-generate it if it's missing or outdated.
|
||||
* - Delete it if we don't have custom translations.
|
||||
*
|
||||
* We will also sync the custom file when a native file is passed
|
||||
* because the custom file might never be loaded if it's missing.
|
||||
*
|
||||
* @param string|false $filePath
|
||||
* @param string $domain
|
||||
*/
|
||||
public function sync( $filePath, $domain ) {
|
||||
if ( ! $filePath ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$locale = $this->fileLocale->get( $filePath, $domain );
|
||||
$filePath = $this->getCustomFilePath( $filePath, $domain, $locale );
|
||||
$lastDbUpdate = $this->translationUpdates->getTimestamp( $domain, $locale );
|
||||
$fileTimestamp = file_exists( $filePath ) ? filemtime( $filePath ) : 0;
|
||||
|
||||
if ( 0 === $lastDbUpdate ) {
|
||||
if ( $fileTimestamp ) {
|
||||
$this->manager->remove( $domain, $locale );
|
||||
}
|
||||
} elseif ( $fileTimestamp < $lastDbUpdate ) {
|
||||
$this->manager->add( $domain, $locale );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param string $filePath
|
||||
* @param string $domain
|
||||
* @param string $locale
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function getCustomFilePath( $filePath, $domain, $locale ) {
|
||||
if ( self::isWpmlCustomFile( $filePath ) ) {
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
return $this->manager->getFilepath( $domain, $locale );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $file
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function isWpmlCustomFile( $file ) {
|
||||
return 0 === strpos( $file, WP_LANG_DIR . '/' . Manager::SUB_DIRECTORY );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace WPML\ST\TranslationFile\Sync;
|
||||
|
||||
use WPML\Collect\Support\Collection;
|
||||
|
||||
class TranslationUpdates {
|
||||
|
||||
// The global constant is not defined yet.
|
||||
const ICL_STRING_TRANSLATION_COMPLETE = 10;
|
||||
|
||||
/** @var \wpdb */
|
||||
private $wpdb;
|
||||
|
||||
/** @var \WPML_Language_Records */
|
||||
private $languageRecords;
|
||||
|
||||
/** @var null|Collection */
|
||||
private $data;
|
||||
|
||||
public function __construct( \wpdb $wpdb, \WPML_Language_Records $languageRecords ) {
|
||||
$this->wpdb = $wpdb;
|
||||
$this->languageRecords = $languageRecords;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $domain
|
||||
* @param string $locale
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getTimestamp( $domain, $locale ) {
|
||||
$this->loadData();
|
||||
$lang = $this->languageRecords->get_language_code( $locale );
|
||||
return (int) $this->data->get( "$lang#$domain" );
|
||||
}
|
||||
|
||||
private function loadData() {
|
||||
if ( ! $this->data ) {
|
||||
$sql = "
|
||||
SELECT
|
||||
CONCAT(st.language,'#',s.context) AS lang_domain,
|
||||
UNIX_TIMESTAMP(MAX(st.translation_date)) as last_update
|
||||
FROM {$this->wpdb->prefix}icl_string_translations AS st
|
||||
INNER JOIN {$this->wpdb->prefix}icl_strings AS s
|
||||
ON st.string_id = s.id
|
||||
WHERE st.value IS NOT NULL AND st.status = %d
|
||||
GROUP BY s.context, st.language;
|
||||
";
|
||||
|
||||
$this->data = wpml_collect(
|
||||
$this->wpdb->get_results( $this->wpdb->prepare( $sql, self::ICL_STRING_TRANSLATION_COMPLETE ) )
|
||||
)->pluck( 'last_update', 'lang_domain' );
|
||||
}
|
||||
}
|
||||
|
||||
public function reset() {
|
||||
$this->data = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace WPML\ST\TranslationFile;
|
||||
|
||||
use stdClass;
|
||||
use WPML\Collect\Support\Collection;
|
||||
use WPML\Element\API\Languages;
|
||||
|
||||
class UpdateHooks implements \IWPML_Action {
|
||||
|
||||
/** @var Manager $file_manager */
|
||||
private $file_manager;
|
||||
|
||||
/** @var DomainsLocalesMapper $domains_locales_mapper */
|
||||
private $domains_locales_mapper;
|
||||
|
||||
/** @var array $updated_translation_ids */
|
||||
private $updated_translation_ids = [];
|
||||
|
||||
/** @var Collection $entities_to_update */
|
||||
private $entities_to_update;
|
||||
|
||||
/** @var callable */
|
||||
private $resetDomainsCache;
|
||||
|
||||
public function __construct(
|
||||
Manager $file_manager,
|
||||
DomainsLocalesMapper $domains_locales_mapper,
|
||||
callable $resetDomainsCache = null
|
||||
) {
|
||||
$this->file_manager = $file_manager;
|
||||
$this->domains_locales_mapper = $domains_locales_mapper;
|
||||
$this->entities_to_update = wpml_collect( [] );
|
||||
$this->resetDomainsCache = $resetDomainsCache ?: [ Domains::class, 'resetCache' ];
|
||||
}
|
||||
|
||||
public function add_hooks() {
|
||||
add_action( 'wpml_st_add_string_translation', array( $this, 'add_to_update_queue' ) );
|
||||
add_action( 'wpml_st_update_string', array( $this, 'refresh_after_update_original_string' ), 10, 6 );
|
||||
add_action( 'wpml_st_before_remove_strings', array( $this, 'refresh_before_remove_strings' ) );
|
||||
/**
|
||||
* @see UpdateHooks::refresh_domain
|
||||
* @since @3.1.0
|
||||
*/
|
||||
add_action( 'wpml_st_refresh_domain', [ $this, 'refresh_domain' ] );
|
||||
|
||||
if ( ! $this->file_manager->isPartialFile() ) {
|
||||
add_action( 'wpml_st_translations_file_post_import', array( $this, 'update_imported_file' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/** @param int $string_translation_id */
|
||||
public function add_to_update_queue( $string_translation_id ) {
|
||||
if ( ! in_array( $string_translation_id, $this->updated_translation_ids, true ) ) {
|
||||
$this->updated_translation_ids[] = $string_translation_id;
|
||||
$this->add_shutdown_action();
|
||||
}
|
||||
}
|
||||
|
||||
private function add_shutdown_action() {
|
||||
if ( ! has_action( 'shutdown', array( $this, 'process_update_queue' ) ) ) {
|
||||
add_action( 'shutdown', array( $this, 'process_update_queue' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function process_update_queue() {
|
||||
call_user_func( $this->resetDomainsCache );
|
||||
|
||||
$outdated_entities = $this->domains_locales_mapper->get_from_translation_ids( $this->updated_translation_ids );
|
||||
$this->entities_to_update = $this->entities_to_update->merge( $outdated_entities );
|
||||
|
||||
$this->entities_to_update->each(
|
||||
function( $entity ) {
|
||||
$this->update_file( $entity->domain, $entity->locale );
|
||||
}
|
||||
);
|
||||
|
||||
return $this->entities_to_update->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $domain
|
||||
* @param string $name
|
||||
* @param string $old_value
|
||||
* @param string $new_value
|
||||
* @param bool|false $force_complete
|
||||
* @param stdClass $string
|
||||
*/
|
||||
public function refresh_after_update_original_string( $domain, $name, $old_value, $new_value, $force_complete, $string ) {
|
||||
$outdated_entities = $this->domains_locales_mapper->get_from_string_ids( [ $string->id ] );
|
||||
$this->entities_to_update = $this->entities_to_update->merge( $outdated_entities );
|
||||
$this->add_shutdown_action();
|
||||
}
|
||||
|
||||
public function update_imported_file( \WPML_ST_Translations_File_Entry $file_entry ) {
|
||||
if ( $file_entry->get_status() === \WPML_ST_Translations_File_Entry::IMPORTED ) {
|
||||
$this->update_file( $file_entry->get_domain(), $file_entry->get_file_locale() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It dispatches the regeneration of MO files for a specific domain in all active languages.
|
||||
*
|
||||
* @param string $domain
|
||||
*/
|
||||
public function refresh_domain( $domain ) {
|
||||
$outdated_entities = $this->domains_locales_mapper->get_from_domain(
|
||||
[ Languages::class, 'getActive' ],
|
||||
$domain
|
||||
);
|
||||
$this->entities_to_update = $this->entities_to_update->merge( $outdated_entities );
|
||||
$this->add_shutdown_action();
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to refresh before the strings are deleted,
|
||||
* otherwise we can't determine which domains to refresh.
|
||||
*
|
||||
* @param array $string_ids
|
||||
*/
|
||||
public function refresh_before_remove_strings( array $string_ids ) {
|
||||
$outdated_entities = $this->domains_locales_mapper->get_from_string_ids( $string_ids );
|
||||
$this->entities_to_update = $this->entities_to_update->merge( $outdated_entities );
|
||||
$this->add_shutdown_action();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $domain
|
||||
* @param string $locale
|
||||
*/
|
||||
private function update_file( $domain, $locale ) {
|
||||
/**
|
||||
* It does not matter whether MO/JED file can be handled or not, that's why `remove` method is placed before `handles` check.
|
||||
* If a file is not `handable` then it can still should be removed.
|
||||
*/
|
||||
$this->file_manager->remove( $domain, $locale );
|
||||
if ( $this->file_manager->handles( $domain ) ) {
|
||||
$this->file_manager->add( $domain, $locale );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace WPML\ST\TranslationFile;
|
||||
|
||||
use WPML\ST\MO\File\ManagerFactory;
|
||||
use function WPML\Container\make;
|
||||
|
||||
class UpdateHooksFactory {
|
||||
/** @return UpdateHooks */
|
||||
public static function create() {
|
||||
static $instance;
|
||||
|
||||
if ( ! $instance ) {
|
||||
$instance = make( UpdateHooks::class, [ ':file_manager' => ManagerFactory::create() ] );
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace WPML\ST\JED\Hooks;
|
||||
|
||||
use WPML\ST\TranslationFile\Sync\FileSync;
|
||||
use WPML_ST_JED_Domain;
|
||||
use WPML_ST_JED_File_Manager;
|
||||
use WPML_ST_Script_Translations_Hooks;
|
||||
use WPML_ST_Translations_File_Locale;
|
||||
|
||||
class Sync implements \IWPML_Frontend_Action, \IWPML_Backend_Action, \IWPML_DIC_Action {
|
||||
|
||||
/** @var FileSync */
|
||||
private $fileSync;
|
||||
|
||||
public function __construct( FileSync $fileSync ) {
|
||||
$this->fileSync = $fileSync;
|
||||
}
|
||||
|
||||
public function add_hooks() {
|
||||
add_filter(
|
||||
'load_script_translation_file',
|
||||
[ $this, 'syncCustomJedFile' ],
|
||||
WPML_ST_Script_Translations_Hooks::PRIORITY_OVERRIDE_JED_FILE - 1,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|false $jedFile Path to the translation file to load. False if there isn't one.
|
||||
* @param string $handler Name of the script to register a translation domain to.
|
||||
* @param string $domain The text domain.
|
||||
*/
|
||||
public function syncCustomJedFile( $jedFile, $handler, $domain ) {
|
||||
$this->fileSync->sync( $jedFile, WPML_ST_JED_Domain::get( $domain, $handler ) );
|
||||
|
||||
return $jedFile;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
class WPML_ST_JED_Domain {
|
||||
|
||||
public static function get( $domain, $handler ) {
|
||||
return $domain . '-' . $handler;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use WPML\ST\TranslationFile\StringEntity;
|
||||
|
||||
class WPML_ST_JED_File_Builder extends WPML\ST\TranslationFile\Builder {
|
||||
|
||||
/** @var string $decoded_eot */
|
||||
private $decoded_eot;
|
||||
|
||||
public function __construct() {
|
||||
$this->decoded_eot = json_decode( WPML_ST_Translations_File_JED::DECODED_EOT_CHAR );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StringEntity[] $strings
|
||||
* @return string
|
||||
*/
|
||||
public function get_content( array $strings ) {
|
||||
$data = new stdClass();
|
||||
|
||||
$data->{'translation-revision-date'} = date( 'Y-m-d H:i:sO' );
|
||||
$data->generator = 'WPML String Translation ' . WPML_ST_VERSION;
|
||||
$data->domain = 'messages';
|
||||
$data->locale_data = new stdClass();
|
||||
$data->locale_data->messages = new stdClass();
|
||||
|
||||
$data->locale_data->messages->{WPML_ST_Translations_File_JED::EMPTY_PROPERTY_NAME} = (object) array(
|
||||
'domain' => 'messages',
|
||||
'plural-forms' => $this->plural_form,
|
||||
'lang' => $this->language,
|
||||
);
|
||||
|
||||
foreach ( $strings as $string ) {
|
||||
$original = $this->get_original_with_context( $string );
|
||||
$data->locale_data->messages->{$original} = $string->get_translations();
|
||||
}
|
||||
|
||||
$jed_content = wp_json_encode( $data );
|
||||
|
||||
return preg_replace( '/"' . WPML_ST_Translations_File_JED::EMPTY_PROPERTY_NAME . '"/', '""', $jed_content, 1 );
|
||||
}
|
||||
|
||||
private function get_original_with_context( StringEntity $string ) {
|
||||
if ( $string->get_context() ) {
|
||||
return $string->get_context() . $this->decoded_eot . $string->get_original();
|
||||
}
|
||||
|
||||
return $string->get_original();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use WPML\Collect\Support\Collection;
|
||||
use WPML\ST\TranslationFile\Manager;
|
||||
|
||||
class WPML_ST_JED_File_Manager extends Manager {
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getFileExtension() {
|
||||
return 'json';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isPartialFile() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
protected function getDomains() {
|
||||
return $this->domains->getJEDDomains();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
use WPML\ST\JED\Hooks\Sync;
|
||||
use WPML\ST\TranslationFile\Sync\FileSync;
|
||||
use function WPML\Container\make;
|
||||
use WPML\ST\TranslationFile\UpdateHooks;
|
||||
|
||||
class WPML_ST_Script_Translations_Hooks_Factory implements IWPML_Backend_Action_Loader, IWPML_Frontend_Action_Loader {
|
||||
|
||||
/**
|
||||
* Create hooks.
|
||||
*
|
||||
* @return array|IWPML_Action
|
||||
* @throws \WPML\Auryn\InjectionException Auryn Exception.
|
||||
*/
|
||||
public function create() {
|
||||
$hooks = array();
|
||||
|
||||
$jed_file_manager = make(
|
||||
WPML_ST_JED_File_Manager::class,
|
||||
[ ':builder' => make( WPML_ST_JED_File_Builder::class ) ]
|
||||
);
|
||||
|
||||
$hooks['update'] = $this->get_update_hooks( $jed_file_manager );
|
||||
|
||||
if ( ! wpml_is_ajax() && ! wpml_is_rest_request() ) {
|
||||
$hooks['filtering'] = $this->get_filtering_hooks( $jed_file_manager );
|
||||
}
|
||||
|
||||
if ( WPML\ST\TranslationFile\Hooks::useFileSynchronization() ) {
|
||||
$hooks['sync'] = make(
|
||||
Sync::class,
|
||||
[
|
||||
':fileSync' => make( FileSync::class, [ ':manager' => $jed_file_manager ] ),
|
||||
':manager' => $jed_file_manager,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return $hooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WPML_ST_JED_File_Manager $jed_file_manager
|
||||
*
|
||||
* @return UpdateHooks
|
||||
*/
|
||||
private function get_update_hooks( $jed_file_manager ) {
|
||||
return make(
|
||||
UpdateHooks::class,
|
||||
[ ':file_manager' => $jed_file_manager ]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WPML_ST_JED_File_Manager $jed_file_manager
|
||||
*
|
||||
* @return WPML_ST_Script_Translations_Hooks
|
||||
*/
|
||||
private function get_filtering_hooks( $jed_file_manager ) {
|
||||
return make(
|
||||
WPML_ST_Script_Translations_Hooks::class,
|
||||
[ ':jed_file_manager' => $jed_file_manager ]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
class WPML_ST_Script_Translations_Hooks implements IWPML_Action {
|
||||
|
||||
const PRIORITY_OVERRIDE_JED_FILE = 10;
|
||||
|
||||
/** @var WPML_ST_Translations_File_Dictionary $dictionary */
|
||||
private $dictionary;
|
||||
|
||||
/** @var WPML_ST_JED_File_Manager $jed_file_manager */
|
||||
private $jed_file_manager;
|
||||
|
||||
/** @var WPML_File $wpml_file */
|
||||
private $wpml_file;
|
||||
|
||||
public function __construct(
|
||||
WPML_ST_Translations_File_Dictionary $dictionary,
|
||||
WPML_ST_JED_File_Manager $jed_file_manager,
|
||||
WPML_File $wpml_file
|
||||
) {
|
||||
$this->dictionary = $dictionary;
|
||||
$this->jed_file_manager = $jed_file_manager;
|
||||
$this->wpml_file = $wpml_file;
|
||||
}
|
||||
|
||||
public function add_hooks() {
|
||||
add_filter( 'load_script_translation_file', array( $this, 'override_jed_file' ), self::PRIORITY_OVERRIDE_JED_FILE, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filepath
|
||||
* @param string $handler
|
||||
* @param string $domain
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function override_jed_file( $filepath, $handler, $domain ) {
|
||||
if ( ! $filepath ) {
|
||||
return $filepath;
|
||||
}
|
||||
|
||||
$locale = $this->get_file_locale( $filepath, $domain );
|
||||
$domain = WPML_ST_JED_Domain::get( $domain, $handler );
|
||||
$wpml_filepath = $this->jed_file_manager->get( $domain, $locale );
|
||||
|
||||
if ( $wpml_filepath ) {
|
||||
return $wpml_filepath;
|
||||
}
|
||||
|
||||
return $filepath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filepath
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_file_imported( $filepath ) {
|
||||
$relative_path = $this->wpml_file->get_relative_path( $filepath );
|
||||
$file = $this->dictionary->find_file_info_by_path( $relative_path );
|
||||
$statuses = array( WPML_ST_Translations_File_Entry::IMPORTED, WPML_ST_Translations_File_Entry::FINISHED );
|
||||
|
||||
return $file && in_array( $file->get_status(), $statuses, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filepath
|
||||
* @param string $domain
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_file_locale( $filepath, $domain ) {
|
||||
return \WPML\Container\make( \WPML_ST_Translations_File_Locale::class )->get( $filepath, $domain );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user