wenpai.org/wp-content/plugins/glotpress/gp-includes/things/translation-set.php
2024-07-04 03:58:20 +08:00

824 lines
22 KiB
PHP

<?php
/**
* Things: GP_Translation_Set class
*
* @package GlotPress
* @subpackage Things
* @since 1.0.0
*/
/**
* Core class used to implement the translation sets.
*
* @since 1.0.0
*/
class GP_Translation_Set extends GP_Thing {
var $table_basename = 'gp_translation_sets';
var $field_names = array( 'id', 'name', 'slug', 'project_id', 'locale' );
var $non_db_field_names = array( 'current_count', 'untranslated_count', 'waiting_count', 'fuzzy_count', 'all_count', 'warnings_count', 'percent_translated', 'wp_locale', 'last_modified' );
var $int_fields = array( 'id', 'project_id' );
var $non_updatable_attributes = array( 'id' );
/**
* ID of the translation set.
*
* @var int
*/
public $id;
/**
* Name of the translation set.
*
* @var string
*/
public $name;
/**
* Slug of the translation set.
*
* @var string
*/
public $slug;
/**
* Project ID of the translation set.
*
* @var int
*/
public $project_id;
/**
* Locale of the translation set.
*
* @var string
*/
public $locale;
/**
* GP project of the translation set.
*
* @var GP_Project
*/
public $project;
/**
* Number of waiting translations.
*
* @var int
*/
public $waiting_count;
/**
* Number of changed requested translations.
*
* @var int
*/
public $changesrequested_count;
/**
* Number of fuzzy translations.
*
* @var int
*/
public $fuzzy_count;
/**
* Number of untranslated originals.
*
* @var int
*/
public $untranslated_count;
/**
* Number of current translations.
*
* @var int
*/
public $current_count;
/**
* Number of translations with warnings.
*
* @var int
*/
public $warnings_count;
/**
* Number of rejected translations.
*
* @var int
*/
public $rejected_count;
/**
* Number of old translations.
*
* @var int
*/
public $old_count;
/**
* Number of all originals.
*
* @var int
*/
public $all_count;
/**
* The percent translated.
*
* @var int
*/
public $percent_translated;
/**
* The English name of the locale.
*
* @var string
*/
public $name_with_locale;
/**
* The WP locale.
*
* @var string
*/
public $wp_locale;
/**
* Sets restriction rules for fields.
*
* @since 1.0.0
*
* @param GP_Validation_Rules $rules The validation rules instance.
*/
public function restrict_fields( $rules ) {
$rules->name_should_not_be( 'empty' );
$rules->slug_should_not_be( 'empty' );
$rules->locale_should_not_be( 'empty' );
$rules->project_id_should_not_be( 'empty' );
}
/**
* Normalizes an array with key-value pairs representing
* a GP_Translation_Set object.
*
* @since 1.0.0
*
* @param array $args Arguments for a GP_Translation_Set object.
* @return array Normalized arguments for a GP_Translation_Set object.
*/
public function normalize_fields( $args ) {
$args = parent::normalize_fields( $args );
if ( isset( $args['name'] ) && empty( $args['name'] ) ) {
if ( isset( $args['locale'] ) && ! empty( $args['locale'] ) ) {
$locale = GP_locales::by_slug( $args['locale'] );
$args['name'] = $locale->english_name;
}
}
if ( isset( $args['slug'] ) && ! $args['slug'] ) {
$args['slug'] = 'default';
}
if ( ! empty( $args['slug'] ) ) {
$args['slug'] = gp_sanitize_slug( $args['slug'] );
}
return $args;
}
/**
* Returns the English name of a locale.
*
* If the slug of the locale is not 'default' then the name of the
* current translation sets gets added as a suffix.
*
* @since 1.0.0
*
* @param string $separator Separator, in case the slug is not 'default'. Default: '&rarr;'.
* @return string The English name of a locale.
*/
public function name_with_locale( $separator = '&rarr;' ) {
$locale = GP_Locales::by_slug( $this->locale );
$parts = array( $locale->english_name );
if ( 'default' !== $this->slug ) {
$parts[] = $this->name;
}
return implode( '&nbsp;' . $separator . '&nbsp;', $parts );
}
public function by_project_id_slug_and_locale( $project_id, $slug, $locale_slug ) {
$result = $this->one(
"SELECT * FROM $this->table
WHERE slug = %s AND project_id= %d AND locale = %s",
$slug,
$project_id,
$locale_slug
);
if ( ! $result && 0 === $project_id ) {
$result = $this->create(
array(
'project_id' => $project_id,
'name' => GP_Locales::by_slug( $locale_slug )->english_name,
'slug' => $slug,
'locale' => $locale_slug,
)
);
}
return $result;
}
public function by_locale( $locale_slug ) {
return $this->many(
"SELECT * FROM $this->table
WHERE locale = %s",
$locale_slug
);
}
public function existing_locales() {
global $wpdb;
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return $wpdb->get_col( "SELECT DISTINCT(locale) FROM $this->table" );
}
public function existing_slugs() {
global $wpdb;
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return $wpdb->get_col( "SELECT DISTINCT(slug) FROM $this->table" );
}
public function by_project_id( $project_id ) {
return $this->many(
"SELECT * FROM $this->table
WHERE project_id = %d ORDER BY name ASC",
$project_id
);
}
/**
* Import translations from a Translations object.
*
* @param Translations $translations the translations to be imported to this translation-set.
* @param string $desired_status 'current', 'waiting' or 'fuzzy'.
* @return boolean or void
*/
public function import( $translations, $desired_status = 'current' ) {
wp_raise_memory_limit( 'gp_translations_import' );
if ( ! isset( $this->project ) || ! $this->project ) {
$this->project = GP::$project->get( $this->project_id );
}
// Fuzzy is also checked in the flags, but if all strings in an import are fuzzy, fuzzy status can be forced.
if ( ! in_array( $desired_status, array( 'current', 'waiting', 'fuzzy' ), true ) ) {
return false;
}
$locale = GP_Locales::by_slug( $this->locale );
$user = wp_get_current_user();
$existing_translations = array();
$current_translations_list = GP::$translation->for_translation(
$this->project,
$this,
'no-limit',
array(
'status' => 'current',
)
);
$existing_translations['current'] = new Translations();
foreach ( $current_translations_list as $entry ) {
$existing_translations['current']->add_entry( $entry );
}
unset( $current_translations_list );
$translations_added = 0;
foreach ( $translations->entries as $entry ) {
if ( empty( $entry->translations ) ) {
continue;
}
$is_fuzzy = in_array( 'fuzzy', $entry->flags, true );
/**
* Filter whether to import fuzzy translations.
*
* @since 1.0.0
*
* @param bool $import_over Import fuzzy translation. Default true.
* @param Translation_Entry $entry Translation entry object to import.
* @param Translations $translations Translations collection.
*/
if ( $is_fuzzy && ! apply_filters( 'gp_translation_set_import_fuzzy_translations', true, $entry, $translations ) ) {
continue;
}
/**
* Filters the the status of imported translations of a translation set.
*
* @since 1.0.0
* @since 2.3.0 Added `$new_translation` and `$old_translation` parameters.
*
* @param string $status The status of imported translations.
* @param Translation_Entry $new_translation Translation entry object to import.
* @param GP_Translation|null $old_translation The previous translation.
*/
$entry->status = apply_filters( 'gp_translation_set_import_status', $is_fuzzy ? 'fuzzy' : $desired_status, $entry, null );
// TODO 文派临时性修复,解决一堆警告问题
// https://github.com/GlotPress/GlotPress/issues/1853
//$entry->warnings = maybe_unserialize( GP::$translation_warnings->check( $entry->singular, $entry->plural, $entry->translations, $locale ) );
$entry->warnings = null;
if ( ! empty( $entry->warnings ) && 'current' === $entry->status ) {
$entry->status = 'waiting';
}
// Lazy load other entries.
if ( ! isset( $existing_translations[ $entry->status ] ) ) {
$existing_translations_list = GP::$translation->for_translation(
$this->project,
$this,
'no-limit',
array(
'status' => $entry->status,
)
);
$existing_translations[ $entry->status ] = new Translations();
foreach ( $existing_translations_list as $_entry ) {
$existing_translations[ $entry->status ]->add_entry( $_entry );
}
unset( $existing_translations_list );
}
$create = false;
$translated = $existing_translations[ $entry->status ]->translate_entry( $entry );
if ( 'current' !== $entry->status && ! $translated ) {
// Don't create an entry if it already exists as current.
$translated = $existing_translations['current']->translate_entry( $entry );
}
if ( $translated ) {
// We have the same string translated, so create a new one if they don't match.
$entry->original_id = $translated->original_id;
$translated_is_different = array_pad( $entry->translations, $locale->nplurals, null ) !== $translated->translations;
/**
* Filter whether to import over an existing translation on a translation set.
*
* @since 1.0.0
*
* @param bool $import_over Import over an existing translation.
*/
$create = apply_filters( 'gp_translation_set_import_over_existing', $translated_is_different );
} else {
// we don't have the string translated, let's see if the original is there
$original = GP::$original->by_project_id_and_entry( $this->project->id, $entry, '+active' );
if ( $original ) {
$entry->original_id = $original->id;
$create = true;
}
}
if ( $create ) {
if ( $user ) {
$entry->user_id = $user->ID;
}
$entry->translation_set_id = $this->id;
$entry->status = apply_filters( 'gp_translation_set_import_status', $entry->status, $entry, $translated );
// Check for errors.
$translation = GP::$translation->create( $entry );
if ( is_object( $translation ) ) {
$translation->set_status( $entry->status );
$translations_added += 1;
}
}
}
gp_clean_translation_set_cache( $this->id );
/**
* Fires after translations have been imported to a translation set.
*
* @since 1.0.0
*
* @param int $translation_set The ID of the translation set the import was made into.
*/
do_action( 'gp_translations_imported', $this->id );
return $translations_added;
}
/**
* Retrieves the number of waiting translations.
*
* @return int Number of waiting translations.
*/
public function waiting_count() {
if ( ! isset( $this->waiting_count ) ) {
$this->update_status_breakdown();
}
return $this->waiting_count;
}
/**
* Retrieves the number of "changes requested" translations.
*
* @return int Number of "changes requested" translations.
*/
public function changesrequested_count() {
if ( ! isset( $this->changesrequested_count ) ) {
$this->update_status_breakdown();
}
return $this->changesrequested_count;
}
/**
* Retrieves the number of untranslated originals.
*
* @return int Number of untranslated originals.
*/
public function untranslated_count() {
if ( ! isset( $this->untranslated_count ) ) {
$this->update_status_breakdown();
}
return $this->untranslated_count;
}
/**
* Retrieves the number of fuzzy translations.
*
* @return int Number of fuzzy translations.
*/
public function fuzzy_count() {
if ( ! isset( $this->fuzzy_count ) ) {
$this->update_status_breakdown();
}
return $this->fuzzy_count;
}
/**
* Retrieves the number of current translations.
*
* @return int Number of current translations.
*/
public function current_count() {
if ( ! isset( $this->current_count ) ) {
$this->update_status_breakdown();
}
return $this->current_count;
}
/**
* Retrieves the number of translations with warnings.
*
* @return int Number of translations with warnings.
*/
public function warnings_count() {
if ( ! isset( $this->warnings_count ) ) {
$this->update_status_breakdown();
}
return $this->warnings_count;
}
/**
* Retrieves the number of all originals.
*
* @return int Number of all originals.
*/
public function all_count() {
if ( ! isset( $this->all_count ) ) {
$this->update_status_breakdown();
}
return $this->all_count;
}
/**
* Populates the count properties.
*/
public function update_status_breakdown() {
$counts = wp_cache_get( $this->id, 'translation_set_status_breakdown' );
/*
* The format was changed in 2.1 and 3.0.
*
* In 2.1 the format was changed to an array of objects in the following sequence,
* however not all may exist in the array, so for example, the array may only be 3
* entries long and skip any of the values:
*
* [0] = Current translations
* [1] = Fuzzy translations
* [2] = Rejected translations
* [3] = Old translations
* [4] = Translations with warnings
* [5] = All translations
*
* In 3.0 the untranslated object was added:
*
* [0] = Current translations
* [1] = Fuzzy translations
* [2] = Rejected translations
* [3] = Old translations
* [4] = Translations with warnings
* [5] = Untranslated originals
* [6] = All translations
*
* Version 3.0 also introduced the cache 'version' array entry to allow for easy
* detection of when the cache should be expired due to changes in the cache.
*
* Note: The _version key is unset after the cache is loaded so that it does not
* get used in the for loops when setting the object properties.
*
*/
if ( ! is_array( $counts ) || ! array_key_exists( '_version', $counts ) || GP_CACHE_VERSION !== $counts['_version'] ) {
global $wpdb;
$counts = array();
$status_counts = $wpdb->get_results(
$wpdb->prepare(
"SELECT
t.status AS translation_status,
COUNT(*) AS total,
COUNT( CASE WHEN o.priority = '-2' THEN o.priority END ) AS `hidden`,
COUNT( CASE WHEN o.priority <> '-2' THEN o.priority END ) AS `public`
FROM {$wpdb->gp_translations} AS t
INNER JOIN {$wpdb->gp_originals} AS o ON t.original_id = o.id
WHERE
t.translation_set_id = %d
AND o.status = '+active'
GROUP BY t.status",
$this->id
)
);
if ( $status_counts ) {
$counts = $status_counts;
}
$warnings_counts = $wpdb->get_row(
$wpdb->prepare(
"SELECT
COUNT(*) AS total,
COUNT( CASE WHEN o.priority = '-2' THEN o.priority END ) AS `hidden`,
COUNT( CASE WHEN o.priority <> '-2' THEN o.priority END ) AS `public`
FROM {$wpdb->gp_translations} AS t
INNER JOIN {$wpdb->gp_originals} AS o ON t.original_id = o.id
WHERE
t.translation_set_id = %d AND
o.status = '+active' AND
( t.status = 'current' OR t.status = 'waiting' )
AND warnings IS NOT NULL
AND warnings != ''",
$this->id
)
);
if ( $warnings_counts ) {
$counts[] = (object) array(
'translation_status' => 'warnings',
'total' => (int) $warnings_counts->total,
'hidden' => (int) $warnings_counts->hidden,
'public' => (int) $warnings_counts->public,
);
}
$changesrequested_counts = $wpdb->get_row(
$wpdb->prepare(
"SELECT
COUNT(*) AS total,
COUNT( CASE WHEN o.priority = '-2' THEN o.priority END ) AS `hidden`,
COUNT( CASE WHEN o.priority <> '-2' THEN o.priority END ) AS `public`
FROM {$wpdb->gp_translations} AS t
INNER JOIN {$wpdb->gp_originals} AS o ON t.original_id = o.id
WHERE
t.translation_set_id = %d AND
o.status = '+active' AND
( t.status = 'changesrequested' )",
$this->id
)
);
if ( $changesrequested_counts ) {
$counts[] = (object) array(
'translation_status' => 'changesrequested',
'total' => (int) $changesrequested_counts->total,
'hidden' => (int) $changesrequested_counts->hidden,
'public' => (int) $changesrequested_counts->public,
);
}
$untranslated_counts = $wpdb->get_row(
$wpdb->prepare(
"SELECT
COUNT(*) AS total,
COUNT( CASE WHEN o.priority = '-2' THEN o.priority END ) AS `hidden`,
COUNT( CASE WHEN o.priority <> '-2' THEN o.priority END ) AS `public`
FROM {$wpdb->gp_originals} AS o
LEFT JOIN {$wpdb->gp_translations} AS t ON o.id = t.original_id AND t.translation_set_id = %d AND t.status != 'rejected' AND t.status != 'old'
WHERE
o.project_id = %d AND
o.status = '+active' AND
t.translation_0 IS NULL",
$this->id,
$this->project_id
)
);
if ( $untranslated_counts ) {
$counts[] = (object) array(
'translation_status' => 'untranslated',
'total' => (int) $untranslated_counts->total,
'public' => (int) $untranslated_counts->public,
'hidden' => (int) $untranslated_counts->hidden,
);
}
$counts['_version'] = GP_CACHE_VERSION;
wp_cache_set( $this->id, $counts, 'translation_set_status_breakdown' );
}
if ( array_key_exists( '_version', $counts ) ) {
unset( $counts['_version'] );
}
$all_count = GP::$original->count_by_project_id( $this->project_id, 'all' );
$counts[] = (object) array(
'translation_status' => 'all',
'total' => $all_count->total,
'hidden' => $all_count->hidden,
'public' => $all_count->public,
);
$statuses = GP::$translation->get_static( 'statuses' );
$statuses[] = 'warnings';
$statuses[] = 'untranslated';
$statuses[] = 'all';
foreach ( $statuses as $status ) {
$this->{$status . '_count'} = 0;
}
$user_can_view_hidden = GP::$permission->current_user_can( 'write', 'project', $this->project_id );
foreach ( $counts as $count ) {
if ( in_array( $count->translation_status, $statuses, true ) ) {
$this->{$count->translation_status . '_count'} = $user_can_view_hidden ? (int) $count->total : (int) $count->public;
}
}
}
/**
* Copies translations from a translation set to the current one
*
* This function doesn't merge then, just copies unconditionally. If a translation already exists, it will be duplicated.
* When copying translations from another project, it will search to find the original first.
*/
public function copy_translations_from( $source_translation_set_id ) {
global $wpdb;
$current_date = $this->now_in_mysql_format();
$source_set = GP::$translation_set->get( $source_translation_set_id );
if ( $source_set->project_id != $this->project_id ) {
$translations = GP::$translation->find_many_no_map( "translation_set_id = '{$source_set->id}'" );
foreach ( $translations as $entry ) {
$source_original = GP::$original->get( $entry->original_id );
$original = GP::$original->by_project_id_and_entry( $this->project_id, $source_original );
if ( $original ) {
$entry->original_id = $original->id;
$entry->translation_set_id = $this->id;
GP::$translation->create( $entry );
}
}
} else {
return $this->query(
"INSERT INTO $wpdb->gp_translations (
original_id, translation_set_id, translation_0, translation_1, translation_2, user_id, status, date_added, date_modified, warnings
)
SELECT
original_id, %s AS translation_set_id, translation_0, translation_1, translation_2, user_id, status, date_added, %s AS date_modified, warnings
FROM $wpdb->gp_translations WHERE translation_set_id = %s",
$this->id,
$current_date,
$source_translation_set_id
);
}
}
public function percent_translated() {
$original_counts = GP::$original->count_by_project_id( $this->project_id, 'all' );
if ( GP::$permission->current_user_can( 'write', 'project', $this->project_id ) ) {
$original_count = $original_counts->total;
} else {
$original_count = $original_counts->public;
}
return $original_count ? floor( $this->current_count() / $original_count * 100 ) : 0;
}
/**
* Retrieves the last modified date of a translation in this translation set.
*
* @since 1.0.0
*
* @return string|false The last modified date on success, false on failure.
*/
public function last_modified() {
return GP::$translation->last_modified( $this );
}
/**
* Deletes a translation set and all of its translations and glossaries.
*
* @since 2.0.0
*
* @return bool
*/
public function delete() {
GP::$translation->delete_many( array( 'translation_set_id' => $this->id ) );
GP::$glossary->delete_many( array( 'translation_set_id', $this->id ) );
return parent::delete();
}
/**
* Executes after creating a translation set.
*
* @since 3.0.0
*
* @return bool
*/
public function after_create() {
/**
* Fires after creating a translation set.
*
* @since 3.0.0
*
* @param GP_Translation_Set $translation_set The translation set that was created.
*/
do_action( 'gp_translation_set_created', $this );
return true;
}
/**
* Executes after saving a translation set.
*
* @since 3.0.0
*
* @param GP_Translation_Set $translation_set_before Translation set before the update.
* @return bool
*/
public function after_save( $translation_set_before ) {
/**
* Fires after saving a translation set.
*
* @since 3.0.0
*
* @param GP_Translation_Set $translation_set Translation set following the update.
* @param GP_Translation_Set $translation_set_before Translation set before the update.
*/
do_action( 'gp_translation_set_saved', $this, $translation_set_before );
return true;
}
/**
* Executes after deleting a translation set.
*
* @since 3.0.0
*
* @return bool
*/
public function after_delete() {
/**
* Fires after deleting a translation set.
*
* @since 3.0.0
*
* @param GP_Translation_Set $translation_set The translation set that was deleted.
*/
do_action( 'gp_translation_set_deleted', $this );
return true;
}
}
GP::$translation_set = new GP_Translation_Set();