This commit is contained in:
hanyixuanten
2026-07-25 23:38:17 +08:00
commit 77ce36cdfe
15 changed files with 740 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
/vendor/
/node_modules/
/.phpunit.result.cache
+49
View File
@@ -0,0 +1,49 @@
# wp-translate Agent Guide
## Project Scope
Build a WordPress multilingual plugin that uses the [Baidu Translate API](https://fanyi-api.baidu.com/doc/23). Support language-specific subdirectory URLs and domain bindings. Translate WordPress posts and pages, interface titles and body text, SEO fields, excerpts, image alt text, tags, and categories.
Unless a user explicitly requests a refresh, each translatable value must be translated once per target language and then persisted on the server for reuse.
## Architecture Boundaries
- Keep language routing, translation providers, persistence, caching, rendering, and admin settings in separate components.
- Put Baidu-specific signing and HTTP behavior behind a provider interface so tests can use a fake provider without credentials or network access.
- Use deterministic translation identities that include the source value fingerprint, source and target language, field context, and provider/configuration version.
- Persist source language, target language, source fingerprint, translated value, status, and necessary timestamps. Reuse a valid persisted translation before making an API request.
- Invalidate a translation when its source content or configured translatable field changes. Never translate dynamically on every front-end request.
- Make failed translation requests non-fatal: retain a prior valid translation when available; otherwise render the configured source-language fallback.
## WordPress Integration
- Follow WordPress Coding Standards and use WordPress APIs for hooks, settings, options, metadata, rewrite rules, sanitization, escaping, capabilities, nonces, HTTP, and cron/queues.
- Do not modify WordPress core, themes, or third-party plugins.
- Use a single plugin prefix for PHP symbols, options, meta keys, transients, REST routes, and custom database tables; document the chosen prefix once it exists.
- Preserve Gutenberg block markup, HTML structure, shortcodes, placeholders, protected tokens, and URLs when translating content.
- Treat serialized data, executable code, credentials, identifiers, passwords, and arbitrary metadata as non-translatable unless explicitly supported.
- Support classic and block themes without assuming a particular page builder.
## Routing And SEO
- Validate every language code against the configured language list; do not trust raw request input.
- Select one canonical URL strategy for each language: subdirectory, domain binding, or an explicitly configured hybrid. Avoid serving the same translation at multiple canonical URLs.
- Generate language-aware permalinks, canonical URLs, `hreflang` alternates, and compatible sitemap output where applicable.
- Preserve query variables, pagination, previews, and supported REST routes through language resolution.
- Register rewrite rules using WordPress APIs. Flush rewrites only on activation, deactivation, or an explicit routing configuration change.
## Security And Privacy
- Store Baidu credentials through WordPress settings APIs and never expose them in front-end output, JavaScript, logs, exports, or ordinary error messages.
- Use `wp_remote_*` with explicit timeouts and actionable, sanitized error handling.
- Sanitize on write, validate configuration, escape at the rendering boundary, and guard admin actions with capability checks and nonces.
- Do not retain full provider payloads or translated user content in logs unless opt-in debug logging is enabled.
## Quality And Documentation
- Add focused tests for language selection, routing, translation cache reuse and invalidation, persistence, provider signing/error handling, and content/meta filtering.
- Mock Baidu API calls in every test. Tests must not require live credentials or outbound network access.
- Keep changes narrow and avoid unrelated refactors or formatting churn.
- Plugin slug and PHP prefix: `wp-translate` and `wpt_`/`WPT_`. The current minimum supported PHP version is 8.1 and the current minimum WordPress version is 6.4.
- Composer is not installed in the initial development environment. Until a WordPress test suite is added, validate syntax with `find . -path './.git' -prune -o -type f -name '*.php' -print0 | xargs -0 -n1 php -l`.
- Document supported languages, routing modes, data retention, translation lifecycle, and known limitations in the README whenever the implementation changes.
+64
View File
@@ -0,0 +1,64 @@
# WP Translate
WP Translate is a WordPress plugin skeleton for persistent multilingual content using the Baidu Translate API.
## Requirements
- WordPress 6.4 or later.
- PHP 8.1 or later.
- A Baidu Translate application ID and secret key.
- WordPress cron enabled, or a real server cron that calls WordPress cron.
## Installation
1. Copy this directory to `wp-content/plugins/wp-translate`.
2. Activate **WP Translate** in WordPress Admin.
3. Open **Settings > WP Translate**.
4. Enter the Baidu credentials, source language, target-language codes, and URL routing mode.
5. Save a post or page to schedule translation jobs for every configured target language.
## Translation Lifecycle
Translations are generated by the `wpt_process_translation` cron task after a post or page is saved. A translation has a deterministic identity based on the source fingerprint, source language, target language, field context, and provider version. The plugin first looks up a completed result in the `{$wpdb->prefix}wpt_translations` table. Only a cache miss causes a request to Baidu.
Changing source text produces a new fingerprint and therefore schedules a new translation without deleting a valid prior translation. Front-end rendering never contacts Baidu; it reads a completed value from the table and otherwise falls back to the source value.
## Supported Content
- Post and page titles, content, and excerpts.
- Attached image `alt` text.
- Assigned categories and tags, including their descriptions.
- Yoast SEO title and description fields.
- Rank Math title and description fields.
Only the listed SEO meta fields are sent for translation. Arbitrary metadata, serialized data, credentials, and code are excluded.
## Routing
The **Subdirectory** mode resolves configured language paths such as `/en/example-post/`. The **Domain binding** mode maps a request host to a language using one `domain=language` entry per line in Settings. Use one canonical strategy for each language to avoid duplicate URLs.
After changing routing settings, rewrite rules are refreshed. The initial implementation does not yet generate translated permalinks, canonical URLs, `hreflang` alternates, redirects, or sitemap entries.
## Security And Data Retention
Credentials are stored in the WordPress `wpt_settings` option and are not rendered on the front end. The plugin stores the translation identity, language pair, field context, source fingerprint, translated value, status, and timestamps. It does not store full Baidu response payloads.
Use HTTPS and restrict database access appropriately because WordPress options are database-backed. Production sites should configure a real cron runner for reliable translation processing.
## Development
The project currently has no Composer dependencies. Validate PHP syntax with:
```sh
find . -path './.git' -prune -o -type f -name '*.php' -print0 | xargs -0 -n1 php -l
```
Before production use, add WordPress PHPUnit integration tests with mocked `wp_remote_post` responses. Required coverage includes language resolution, rewrite behavior, persistence reuse and invalidation, Baidu request signing and errors, content filtering, and fallback rendering.
## Known Limitations
- The settings UI accepts language codes but does not yet validate them against a maintained Baidu language-code list.
- API calls translate raw WordPress content. HTML, blocks, shortcodes, placeholders, and URLs need a protected-token pipeline before production use.
- Taxonomy and SEO values are queued but the current rendering filters cover only a subset of SEO plugins and term views.
- Domain binding assumes the WordPress site and server virtual hosts are already configured for every bound domain.
- Translation failures are stored as failed attempts and render the source value; retry management and an admin job monitor are not implemented yet.
+65
View File
@@ -0,0 +1,65 @@
<?php
defined( 'ABSPATH' ) || exit;
class WPT_Admin {
public function register() {
add_action( 'admin_menu', array( $this, 'add_settings_page' ) );
add_action( 'admin_init', array( $this, 'register_settings' ) );
}
public function add_settings_page() {
add_options_page( __( 'WP Translate', 'wp-translate' ), __( 'WP Translate', 'wp-translate' ), 'manage_options', 'wp-translate', array( $this, 'render_settings_page' ) );
}
public function register_settings() {
register_setting( 'wpt_settings', 'wpt_settings', array( $this, 'sanitize_settings' ) );
}
public function sanitize_settings( $settings ) {
$settings = (array) $settings;
$bindings = array();
foreach ( preg_split( '/\r\n|\r|\n/', (string) ( $settings['domain_bindings'] ?? '' ) ) as $line ) {
$parts = array_map( 'trim', explode( '=', $line, 2 ) );
if ( 2 === count( $parts ) && '' !== $parts[0] && '' !== $parts[1] ) {
$bindings[ sanitize_text_field( $parts[0] ) ] = sanitize_key( $parts[1] );
}
}
return array(
'source_language' => sanitize_key( $settings['source_language'] ?? 'zh' ),
'target_languages' => array_values( array_filter( array_map( 'sanitize_key', explode( ',', (string) ( $settings['target_languages'] ?? 'en' ) ) ) ) ),
'routing_mode' => in_array( $settings['routing_mode'] ?? '', array( 'subdirectory', 'domain' ), true ) ? $settings['routing_mode'] : 'subdirectory',
'domain_bindings' => $bindings,
'fallback_language' => sanitize_key( $settings['fallback_language'] ?? 'zh' ),
'baidu_app_id' => sanitize_text_field( $settings['baidu_app_id'] ?? '' ),
'baidu_secret_key' => sanitize_text_field( $settings['baidu_secret_key'] ?? '' ),
);
}
public function render_settings_page() {
$settings = WPT_Settings::get();
$bindings = array();
foreach ( (array) $settings['domain_bindings'] as $domain => $language ) {
$bindings[] = $domain . '=' . $language;
}
?>
<div class="wrap">
<h1><?php esc_html_e( 'WP Translate', 'wp-translate' ); ?></h1>
<form action="options.php" method="post">
<?php settings_fields( 'wpt_settings' ); ?>
<table class="form-table" role="presentation">
<tr><th scope="row"><label for="wpt-source-language"><?php esc_html_e( 'Source language', 'wp-translate' ); ?></label></th><td><input id="wpt-source-language" name="wpt_settings[source_language]" type="text" value="<?php echo esc_attr( $settings['source_language'] ); ?>" /></td></tr>
<tr><th scope="row"><label for="wpt-target-languages"><?php esc_html_e( 'Target languages', 'wp-translate' ); ?></label></th><td><input id="wpt-target-languages" name="wpt_settings[target_languages]" type="text" value="<?php echo esc_attr( implode( ',', $settings['target_languages'] ) ); ?>" class="regular-text" /></td></tr>
<tr><th scope="row"><label for="wpt-routing-mode"><?php esc_html_e( 'URL mode', 'wp-translate' ); ?></label></th><td><select id="wpt-routing-mode" name="wpt_settings[routing_mode]"><option value="subdirectory" <?php selected( $settings['routing_mode'], 'subdirectory' ); ?>><?php esc_html_e( 'Subdirectory', 'wp-translate' ); ?></option><option value="domain" <?php selected( $settings['routing_mode'], 'domain' ); ?>><?php esc_html_e( 'Domain binding', 'wp-translate' ); ?></option></select></td></tr>
<tr><th scope="row"><label for="wpt-domain-bindings"><?php esc_html_e( 'Domain bindings', 'wp-translate' ); ?></label></th><td><textarea id="wpt-domain-bindings" name="wpt_settings[domain_bindings]" rows="4" class="large-text"><?php echo esc_textarea( implode( "\n", $bindings ) ); ?></textarea></td></tr>
<tr><th scope="row"><label for="wpt-baidu-app-id"><?php esc_html_e( 'Baidu app ID', 'wp-translate' ); ?></label></th><td><input id="wpt-baidu-app-id" name="wpt_settings[baidu_app_id]" type="text" value="<?php echo esc_attr( $settings['baidu_app_id'] ); ?>" class="regular-text" /></td></tr>
<tr><th scope="row"><label for="wpt-baidu-secret-key"><?php esc_html_e( 'Baidu secret key', 'wp-translate' ); ?></label></th><td><input id="wpt-baidu-secret-key" name="wpt_settings[baidu_secret_key]" type="password" value="<?php echo esc_attr( $settings['baidu_secret_key'] ); ?>" class="regular-text" autocomplete="new-password" /></td></tr>
</table>
<?php submit_button(); ?>
</form>
</div>
<?php
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
defined( 'ABSPATH' ) || exit;
class WPT_Baidu_Provider implements WPT_Translation_Provider {
private $app_id;
private $secret_key;
public function __construct( $app_id, $secret_key ) {
$this->app_id = $app_id;
$this->secret_key = $secret_key;
}
public function get_version() {
return 'baidu-vip-v1';
}
public function translate( $source_value, $source_language, $target_language, $context ) {
if ( '' === $this->app_id || '' === $this->secret_key ) {
return WPT_Translation_Result::failure( 'missing_credentials', __( 'Baidu Translate credentials are not configured.', 'wp-translate' ) );
}
$salt = (string) wp_rand( 100000, 999999 );
$signature = md5( $this->app_id . $source_value . $salt . $this->secret_key );
$response = wp_remote_post(
'https://fanyi-api.baidu.com/api/trans/vip/translate',
array(
'timeout' => 15,
'body' => array(
'q' => $source_value,
'from' => $source_language,
'to' => $target_language,
'appid' => $this->app_id,
'salt' => $salt,
'sign' => $signature,
),
)
);
if ( is_wp_error( $response ) ) {
return WPT_Translation_Result::failure( 'request_failed', $response->get_error_message() );
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! is_array( $body ) || empty( $body['trans_result'] ) || ! is_array( $body['trans_result'] ) ) {
$error_code = isset( $body['error_code'] ) ? sanitize_key( $body['error_code'] ) : 'invalid_response';
$error_message = isset( $body['error_msg'] ) ? sanitize_text_field( $body['error_msg'] ) : __( 'Baidu Translate returned an invalid response.', 'wp-translate' );
return WPT_Translation_Result::failure( $error_code, $error_message );
}
$translated_parts = wp_list_pluck( $body['trans_result'], 'dst' );
return WPT_Translation_Result::success( implode( "\n", $translated_parts ) );
}
}
+178
View File
@@ -0,0 +1,178 @@
<?php
defined( 'ABSPATH' ) || exit;
class WPT_Content_Controller {
private $store;
private $router;
public function __construct( WPT_Translation_Store $store, WPT_Language_Router $router ) {
$this->store = $store;
$this->router = $router;
}
public function register() {
add_action( 'save_post', array( $this, 'schedule_post_translation' ), 20, 3 );
add_action( 'wpt_process_translation', array( $this, 'translate_post' ), 10, 2 );
add_filter( 'the_title', array( $this, 'translate_title' ), 20, 2 );
add_filter( 'the_content', array( $this, 'translate_content' ), 20 );
add_filter( 'get_the_excerpt', array( $this, 'translate_excerpt' ), 20, 2 );
add_filter( 'wp_get_attachment_image_attributes', array( $this, 'translate_image_alt' ), 20, 2 );
add_filter( 'single_term_title', array( $this, 'translate_term_title' ), 20, 2 );
add_filter( 'term_description', array( $this, 'translate_term_description' ), 20, 3 );
add_filter( 'wpseo_title', array( $this, 'translate_seo_value' ), 20 );
add_filter( 'wpseo_metadesc', array( $this, 'translate_seo_value' ), 20 );
add_filter( 'rank_math/frontend/title', array( $this, 'translate_seo_value' ), 20 );
add_filter( 'rank_math/frontend/description', array( $this, 'translate_seo_value' ), 20 );
}
public function schedule_post_translation( $post_id, $post, $update ) {
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) || ! in_array( $post->post_type, array( 'post', 'page' ), true ) ) {
return;
}
foreach ( WPT_Settings::target_languages() as $target_language ) {
if ( ! wp_next_scheduled( 'wpt_process_translation', array( $post_id, $target_language ) ) ) {
wp_schedule_single_event( time() + 30, 'wpt_process_translation', array( $post_id, $target_language ) );
}
}
}
public function translate_post( $post_id, $target_language ) {
$post = get_post( $post_id );
$settings = WPT_Settings::get();
if ( ! $post || ! WPT_Settings::is_supported_language( $target_language ) ) {
return;
}
$service = new WPT_Translation_Service(
$this->store,
new WPT_Baidu_Provider( $settings['baidu_app_id'], $settings['baidu_secret_key'] )
);
$fields = array(
'post_title' => $post->post_title,
'post_content' => $post->post_content,
'post_excerpt' => $post->post_excerpt,
);
$seo_meta_keys = array(
'_yoast_wpseo_title',
'_yoast_wpseo_metadesc',
'rank_math_title',
'rank_math_description',
);
foreach ( $seo_meta_keys as $meta_key ) {
$meta_value = get_post_meta( $post_id, $meta_key, true );
if ( is_string( $meta_value ) && '' !== $meta_value ) {
$fields[ 'meta:' . $meta_key ] = $meta_value;
}
}
foreach ( $fields as $field => $value ) {
if ( '' !== $value ) {
$service->get_or_translate( $value, $settings['source_language'], $target_language, 'post:' . $post_id . ':' . $field );
}
}
$this->translate_attachment_alt_text( $post_id, $service, $settings, $target_language );
$this->translate_terms( $post_id, $service, $settings, $target_language );
}
public function translate_title( $title, $post_id ) {
return $this->translated_value( $title, 'post:' . $post_id . ':post_title' );
}
public function translate_content( $content ) {
return $this->translated_value( $content, 'post:' . get_the_ID() . ':post_content' );
}
public function translate_excerpt( $excerpt, $post ) {
return $this->translated_value( $excerpt, 'post:' . $post->ID . ':post_excerpt' );
}
public function translate_image_alt( $attributes, $attachment ) {
if ( empty( $attributes['alt'] ) ) {
return $attributes;
}
$attributes['alt'] = $this->translated_value( $attributes['alt'], 'attachment:' . $attachment->ID . ':alt' );
return $attributes;
}
public function translate_term_title( $term_name, $term ) {
return $this->translated_value( $term_name, 'term:' . $term->term_id . ':name' );
}
public function translate_term_description( $description, $term_id, $taxonomy ) {
return $this->translated_value( $description, 'term:' . $term_id . ':description' );
}
public function translate_seo_value( $value ) {
$post_id = get_queried_object_id();
if ( ! $post_id || '' === $value ) {
return $value;
}
$seo_meta_keys = array(
'_yoast_wpseo_title',
'_yoast_wpseo_metadesc',
'rank_math_title',
'rank_math_description',
);
foreach ( $seo_meta_keys as $meta_key ) {
if ( $value === get_post_meta( $post_id, $meta_key, true ) ) {
return $this->translated_value( $value, 'post:' . $post_id . ':meta:' . $meta_key );
}
}
return $value;
}
private function translated_value( $source_value, $context ) {
$settings = WPT_Settings::get();
$target_language = $this->router->current_language();
if ( $target_language === $settings['source_language'] || '' === $source_value ) {
return $source_value;
}
$provider = new WPT_Baidu_Provider( $settings['baidu_app_id'], $settings['baidu_secret_key'] );
$identity_key = WPT_Translation_Identity::key( $source_value, $settings['source_language'], $target_language, $context, $provider->get_version() );
$translation = $this->store->find_valid( $identity_key );
return ! empty( $translation['translated_value'] ) ? $translation['translated_value'] : $source_value;
}
private function translate_attachment_alt_text( $post_id, $service, $settings, $target_language ) {
$attachments = get_attached_media( 'image', $post_id );
foreach ( $attachments as $attachment ) {
$alt_text = get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true );
if ( is_string( $alt_text ) && '' !== $alt_text ) {
$service->get_or_translate( $alt_text, $settings['source_language'], $target_language, 'attachment:' . $attachment->ID . ':alt' );
}
}
}
private function translate_terms( $post_id, $service, $settings, $target_language ) {
$terms = get_the_terms( $post_id, array( 'category', 'post_tag' ) );
if ( ! is_array( $terms ) ) {
return;
}
foreach ( $terms as $term ) {
if ( '' !== $term->name ) {
$service->get_or_translate( $term->name, $settings['source_language'], $target_language, 'term:' . $term->term_id . ':name' );
}
if ( '' !== $term->description ) {
$service->get_or_translate( $term->description, $settings['source_language'], $target_language, 'term:' . $term->term_id . ':description' );
}
}
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
defined( 'ABSPATH' ) || exit;
class WPT_Language_Router {
public function register() {
add_rewrite_tag( '%wpt_language%', '([a-zA-Z-]+)' );
add_rewrite_rule( '^([a-zA-Z-]+)/(.*)?$', 'index.php?wpt_language=$matches[1]&pagename=$matches[2]', 'top' );
add_filter( 'query_vars', array( $this, 'query_vars' ) );
add_action( 'parse_request', array( $this, 'resolve_domain_language' ) );
}
public function query_vars( $query_vars ) {
$query_vars[] = 'wpt_language';
return $query_vars;
}
public function resolve_domain_language( $wp ) {
$settings = WPT_Settings::get();
$host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
$bindings = (array) $settings['domain_bindings'];
if ( 'domain' === $settings['routing_mode'] && isset( $bindings[ $host ] ) && WPT_Settings::is_supported_language( $bindings[ $host ] ) ) {
$wp->query_vars['wpt_language'] = sanitize_key( $bindings[ $host ] );
}
}
public function current_language() {
$settings = WPT_Settings::get();
$language = get_query_var( 'wpt_language' );
if ( $language && WPT_Settings::is_supported_language( $language ) ) {
return sanitize_key( $language );
}
return sanitize_key( $settings['source_language'] );
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
defined( 'ABSPATH' ) || exit;
class WPT_Plugin {
private static $instance;
private $router;
private $content_controller;
private $admin;
private function __construct() {
$this->router = new WPT_Language_Router();
$this->content_controller = new WPT_Content_Controller( new WPT_Translation_Store(), $this->router );
$this->admin = new WPT_Admin();
add_action( 'init', array( $this, 'register' ) );
add_action( 'update_option_wpt_settings', array( $this, 'refresh_rewrite_rules' ), 10, 2 );
$this->admin->register();
}
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
public static function activate() {
WPT_Translation_Store::install();
add_option(
'wpt_settings',
array(
'source_language' => 'zh',
'target_languages' => array( 'en' ),
'routing_mode' => 'subdirectory',
'domain_bindings' => array(),
'fallback_language' => 'zh',
),
'',
false
);
flush_rewrite_rules();
}
public static function deactivate() {
wp_clear_scheduled_hook( 'wpt_process_translation' );
flush_rewrite_rules();
}
public function register() {
$this->router->register();
$this->content_controller->register();
}
public function refresh_rewrite_rules( $old_value, $new_value ) {
if ( (array) $old_value !== (array) $new_value ) {
flush_rewrite_rules();
}
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
defined( 'ABSPATH' ) || exit;
class WPT_Settings {
public static function get() {
$defaults = array(
'source_language' => 'zh',
'target_languages' => array( 'en' ),
'routing_mode' => 'subdirectory',
'domain_bindings' => array(),
'fallback_language' => 'zh',
'baidu_app_id' => '',
'baidu_secret_key' => '',
);
return wp_parse_args( (array) get_option( 'wpt_settings', array() ), $defaults );
}
public static function target_languages() {
return array_values( array_filter( array_map( 'sanitize_key', (array) self::get()['target_languages'] ) ) );
}
public static function is_supported_language( $language ) {
$settings = self::get();
$languages = array_merge( array( sanitize_key( $settings['source_language'] ) ), self::target_languages() );
return in_array( sanitize_key( $language ), $languages, true );
}
}
@@ -0,0 +1,21 @@
<?php
defined( 'ABSPATH' ) || exit;
class WPT_Translation_Identity {
public static function source_fingerprint( $source_value ) {
return hash( 'sha256', (string) $source_value );
}
public static function key( $source_value, $source_language, $target_language, $context, $provider_version ) {
$parts = array(
self::source_fingerprint( $source_value ),
(string) $source_language,
(string) $target_language,
(string) $context,
(string) $provider_version,
);
return hash( 'sha256', implode( '|', $parts ) );
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
defined( 'ABSPATH' ) || exit;
class WPT_Translation_Result {
public $success;
public $value;
public $error_code;
public $error_message;
private function __construct( $success, $value = '', $error_code = '', $error_message = '' ) {
$this->success = $success;
$this->value = $value;
$this->error_code = $error_code;
$this->error_message = $error_message;
}
public static function success( $value ) {
return new self( true, $value );
}
public static function failure( $error_code, $error_message ) {
return new self( false, '', $error_code, $error_message );
}
}
@@ -0,0 +1,34 @@
<?php
defined( 'ABSPATH' ) || exit;
class WPT_Translation_Service {
private $store;
private $provider;
public function __construct( WPT_Translation_Store $store, WPT_Translation_Provider $provider ) {
$this->store = $store;
$this->provider = $provider;
}
public function get_or_translate( $source_value, $source_language, $target_language, $context ) {
$source_value = (string) $source_value;
$source_fingerprint = WPT_Translation_Identity::source_fingerprint( $source_value );
$identity_key = WPT_Translation_Identity::key( $source_value, $source_language, $target_language, $context, $this->provider->get_version() );
$existing = $this->store->find_valid( $identity_key );
if ( ! empty( $existing['translated_value'] ) ) {
return WPT_Translation_Result::success( $existing['translated_value'] );
}
$result = $this->provider->translate( $source_value, $source_language, $target_language, $context );
if ( $result->success ) {
$this->store->save( $identity_key, $source_language, $target_language, $context, $source_fingerprint, $result->value, 'complete' );
} else {
$this->store->save( $identity_key, $source_language, $target_language, $context, $source_fingerprint, '', 'failed' );
}
return $result;
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
defined( 'ABSPATH' ) || exit;
class WPT_Translation_Store {
public static function table_name() {
global $wpdb;
return $wpdb->prefix . 'wpt_translations';
}
public static function install() {
global $wpdb;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$table_name = self::table_name();
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$table_name} (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
identity_key char(64) NOT NULL,
source_language varchar(20) NOT NULL,
target_language varchar(20) NOT NULL,
field_context varchar(191) NOT NULL,
source_fingerprint char(64) NOT NULL,
translated_value longtext NULL,
status varchar(20) NOT NULL,
created_at datetime NOT NULL,
updated_at datetime NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY identity_key (identity_key),
KEY target_language (target_language),
KEY source_fingerprint (source_fingerprint)
) {$charset_collate};";
dbDelta( $sql );
}
public function find_valid( $identity_key ) {
global $wpdb;
$table_name = self::table_name();
$sql = $wpdb->prepare(
"SELECT * FROM {$table_name} WHERE identity_key = %s AND status = 'complete' LIMIT 1",
$identity_key
);
return $wpdb->get_row( $sql, ARRAY_A );
}
public function save( $identity_key, $source_language, $target_language, $field_context, $source_fingerprint, $translated_value, $status ) {
global $wpdb;
$now = current_time( 'mysql', true );
return $wpdb->replace(
self::table_name(),
array(
'identity_key' => $identity_key,
'source_language' => $source_language,
'target_language' => $target_language,
'field_context' => $field_context,
'source_fingerprint' => $source_fingerprint,
'translated_value' => $translated_value,
'status' => $status,
'created_at' => $now,
'updated_at' => $now,
),
array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
);
}
}
@@ -0,0 +1,9 @@
<?php
defined( 'ABSPATH' ) || exit;
interface WPT_Translation_Provider {
public function get_version();
public function translate( $source_value, $source_language, $target_language, $context );
}
+33
View File
@@ -0,0 +1,33 @@
<?php
/**
* Plugin Name: WP Translate
* Description: Persistent multilingual WordPress translations powered by Baidu Translate.
* Version: 0.1.0
* Requires at least: 6.4
* Requires PHP: 8.1
* Author: WP Translate
* Text Domain: wp-translate
*/
defined( 'ABSPATH' ) || exit;
define( 'WPT_VERSION', '0.1.0' );
define( 'WPT_FILE', __FILE__ );
define( 'WPT_PATH', plugin_dir_path( __FILE__ ) );
require_once WPT_PATH . 'includes/interface-wpt-translation-provider.php';
require_once WPT_PATH . 'includes/class-wpt-translation-result.php';
require_once WPT_PATH . 'includes/class-wpt-translation-identity.php';
require_once WPT_PATH . 'includes/class-wpt-translation-store.php';
require_once WPT_PATH . 'includes/class-wpt-baidu-provider.php';
require_once WPT_PATH . 'includes/class-wpt-translation-service.php';
require_once WPT_PATH . 'includes/class-wpt-settings.php';
require_once WPT_PATH . 'includes/class-wpt-language-router.php';
require_once WPT_PATH . 'includes/class-wpt-content-controller.php';
require_once WPT_PATH . 'includes/class-wpt-admin.php';
require_once WPT_PATH . 'includes/class-wpt-plugin.php';
register_activation_hook( WPT_FILE, array( 'WPT_Plugin', 'activate' ) );
register_deactivation_hook( WPT_FILE, array( 'WPT_Plugin', 'deactivate' ) );
add_action( 'plugins_loaded', array( 'WPT_Plugin', 'instance' ) );