From 4a647400ff4639d2c72ca94debcbc19f55c742b0 Mon Sep 17 00:00:00 2001 From: YvetteNikolov Date: Tue, 10 Nov 2026 16:24:58 +0100 Subject: [PATCH 1/3] feat: remove translation cache admin column The column added a wide column to every public post type list screen. The uncached and incomplete request counters only fed this column, so they go with it instead of writing post meta on every visitor request. --- README.md | 11 -- assets/css/admin-columns.css | 42 ----- dist/admin-columns.asset.php | 1 - dist/admin-columns.css | 1 - dist/admin-columns.js | 0 readme.txt | 11 -- src/Bootstrap.php | 2 - src/Controllers/RestAPIController.php | 4 - src/Providers/AdminColumnsServiceProvider.php | 160 ------------------ src/Repositories/TranslationRepository.php | 97 +---------- src/Services/TranslationService.php | 10 -- src/Traits/CachePostTypesTrait.php | 3 - .../Unit/AdminColumnsServiceProviderTest.php | 37 ---- tests/Unit/RestAPIControllerTest.php | 10 -- tests/Unit/TranslationRepositoryTest.php | 23 +-- tests/Unit/TranslationServiceTest.php | 9 +- webpack.config.js | 1 - 17 files changed, 5 insertions(+), 417 deletions(-) delete mode 100644 assets/css/admin-columns.css delete mode 100644 dist/admin-columns.asset.php delete mode 100644 dist/admin-columns.css delete mode 100644 dist/admin-columns.js delete mode 100644 src/Providers/AdminColumnsServiceProvider.php delete mode 100644 tests/Unit/AdminColumnsServiceProviderTest.php diff --git a/README.md b/README.md index 98bfaa3..2414444 100644 --- a/README.md +++ b/README.md @@ -35,17 +35,6 @@ A metabox labeled **Yard DeepL** is displayed on the edit screen of supported po - **Disable translation cache:** When checked, the cache is bypassed for this object. Useful for posts with dynamic content that should always be translated fresh. - **Clear translation cache:** When checked and the post is saved, all cached translations for this object are deleted. -### Admin: Translation Cache Column - -A **Translation cache** column is added to the post list screen of all supported post types. Its purpose is to notify editors which posts should be cached as soon as possible to avoid unnecessary DeepL API costs. - -- **Green badge per language** (e.g. `NL`, `EN-US`): a valid, up-to-date cached translation exists for that language. Visitor requests are served from cache at no cost. -- **Yellow badge with a count per language** (e.g. `EN-US 3`): a cached translation exists, but that many visitor requests received only part of their texts because the rest is missing from the cache. Hovering the badge shows the full count as a tooltip. An editor visiting the page translates the missing texts, stores them and resets the count. -- **Grey badge with a count per language** (e.g. `EN-US 42`): no cache exists for that language and visitors have triggered that many live DeepL API calls. Hovering the badge shows the full count as a tooltip. These posts are the most urgent to cache. -- **—**: the post has never been requested for translation, or caching is disabled for this post. - -The uncached call count is only incremented for visitor requests (users without cache-write capability) and is automatically reset per language once a cache entry is stored. The same applies to the incomplete response count, which is also removed when the cache is cleared. Posts with the **Disable translation cache** option enabled are excluded from the column entirely. - ## External Services This plugin connects to the DeepL API to provide translations for content. diff --git a/assets/css/admin-columns.css b/assets/css/admin-columns.css deleted file mode 100644 index 37bec44..0000000 --- a/assets/css/admin-columns.css +++ /dev/null @@ -1,42 +0,0 @@ -.ydpl-cache-badges { - display: flex; - flex-wrap: wrap; - gap: 3px; -} - -.ydpl-cache-badge { - display: inline-flex; - align-items: center; - gap: 4px; - font-size: 10px; - line-height: 1; - padding: 3px 6px; - border-radius: 3px; - font-weight: 600; - letter-spacing: 0.03em; -} - -.ydpl-cache-badge--cached { - background: #00a32a; - color: #fff; -} - -.ydpl-cache-badge--incomplete { - background: #dba617; - color: #1d2327; -} - -.ydpl-cache-badge--uncached { - background: #f0f0f1; - color: #3c434a; -} - -.ydpl-badge-count { - font-weight: 400; - font-size: 9px; - opacity: 0.7; -} - -.ydpl-cache-status--none { - color: #8c8f94; -} diff --git a/dist/admin-columns.asset.php b/dist/admin-columns.asset.php deleted file mode 100644 index bb0a903..0000000 --- a/dist/admin-columns.asset.php +++ /dev/null @@ -1 +0,0 @@ - array(), 'version' => '0921bcaa4b4ca4ac535b'); diff --git a/dist/admin-columns.css b/dist/admin-columns.css deleted file mode 100644 index 8d7fb5d..0000000 --- a/dist/admin-columns.css +++ /dev/null @@ -1 +0,0 @@ -.ydpl-cache-badges{display:flex;flex-wrap:wrap;gap:3px}.ydpl-cache-badge{align-items:center;border-radius:3px;display:inline-flex;font-size:10px;font-weight:600;gap:4px;letter-spacing:.03em;line-height:1;padding:3px 6px}.ydpl-cache-badge--cached{background:#00a32a;color:#fff}.ydpl-cache-badge--incomplete{background:#dba617;color:#1d2327}.ydpl-cache-badge--uncached{background:#f0f0f1;color:#3c434a}.ydpl-badge-count{font-size:9px;font-weight:400;opacity:.7}.ydpl-cache-status--none{color:#8c8f94} diff --git a/dist/admin-columns.js b/dist/admin-columns.js deleted file mode 100644 index e69de29..0000000 diff --git a/readme.txt b/readme.txt index 709b60b..3afee22 100644 --- a/readme.txt +++ b/readme.txt @@ -33,17 +33,6 @@ A metabox labeled Yard DeepL is displayed on the edit screen of supported post t * Disable translation cache: When checked, the cache is bypassed for this object. Useful for posts with dynamic content that should always be translated fresh. * Clear translation cache: When checked and the post is saved, all cached translations for this object are deleted. -= Admin: Translation Cache Column = - -A Translation cache column is added to the post list screen of all supported post types. Its purpose is to notify editors which posts should be cached as soon as possible to avoid unnecessary DeepL API costs. - -* Green badge per language (e.g. NL, EN-US): a valid, up-to-date cached translation exists for that language. Visitor requests are served from cache at no cost. -* Yellow badge with a count per language (e.g. EN-US 3): a cached translation exists, but that many visitor requests received only part of their texts because the rest is missing from the cache. Hovering the badge shows the full count as a tooltip. An editor visiting the page translates the missing texts, stores them and resets the count. -* Grey badge with a count per language (e.g. EN-US 42): no cache exists for that language and visitors have triggered that many live DeepL API calls. Hovering the badge shows the full count as a tooltip. These posts are the most urgent to cache. -* —: the post has never been requested for translation, or caching is disabled for this post. - -The uncached call count is only incremented for visitor requests (users without cache-write capability) and is automatically reset per language once a cache entry is stored. The same applies to the incomplete response count, which is also removed when the cache is cleared. Posts with the Disable translation cache option enabled are excluded from the column entirely. - == External Services == This plugin connects to the DeepL API to provide translations for content. diff --git a/src/Bootstrap.php b/src/Bootstrap.php index 1c0cfaf..20abea2 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -9,7 +9,6 @@ exit; } -use YDPL\Providers\AdminColumnsServiceProvider; use YDPL\Providers\AssetsServiceProvider; use YDPL\Providers\MetaBoxServiceProvider; use YDPL\Providers\RestAPIServiceProvider; @@ -72,7 +71,6 @@ protected function get_providers(): array new SettingsServiceProvider(), new RestAPIServiceProvider(), new MetaBoxServiceProvider(), - new AdminColumnsServiceProvider(), ); } diff --git a/src/Controllers/RestAPIController.php b/src/Controllers/RestAPIController.php index a615663..67d4d9e 100644 --- a/src/Controllers/RestAPIController.php +++ b/src/Controllers/RestAPIController.php @@ -68,10 +68,6 @@ public function handle_translate_request( WP_REST_Request $request ): WP_REST_Re if ( ! $user_has_cache_capability && array() !== $missing_texts ) { $cached_texts = $this->service->translations_for( $text, $cached_translation ?? array() ); - if ( array() !== $cached_texts ) { - $this->service->increment_incomplete_request_count( $object_id, $target_lang ); - } - if ( $this->is_rate_limit_exceeded() ) { return array() !== $cached_texts ? new WP_REST_Response( $cached_texts ) diff --git a/src/Providers/AdminColumnsServiceProvider.php b/src/Providers/AdminColumnsServiceProvider.php deleted file mode 100644 index d720f68..0000000 --- a/src/Providers/AdminColumnsServiceProvider.php +++ /dev/null @@ -1,160 +0,0 @@ -repository = new TranslationRepository(); - } - - /** - * @since 2.0.0 - */ - public function register(): void - { - add_action( 'admin_init', array( $this, 'register_columns' ) ); - add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) ); - } - - /** - * Must run after every post type is registered on init. - * - * @since 2.2.0 - */ - public function register_columns(): void - { - foreach ( $this->get_cache_metabox_post_types() as $post_type ) { - add_filter( "manage_{$post_type}_posts_columns", array( $this, 'add_column' ) ); - add_action( "manage_{$post_type}_posts_custom_column", array( $this, 'render_column' ), 10, 2 ); - } - } - - /** - * @since 2.0.0 - */ - public function add_column( array $columns ): array - { - $columns['ydpl_translation_cache'] = __( 'Translation cache', 'yard-deepl' ); - - return $columns; - } - - /** - * @since 2.0.0 - */ - public function render_column( string $column, int $post_id ): void - { - if ( 'ydpl_translation_cache' !== $column ) { - return; - } - - $configured_languages = ydpl_resolve_from_container( 'ydpl.site_options' )->configured_supported_languages(); - - if ( array() === $configured_languages ) { - echo '—'; - - return; - } - - $column_data = $this->repository->get_column_data( $post_id, $configured_languages ); - $cached_langs = $column_data['cached_languages']; - $uncached_counts = $column_data['uncached_request_counts']; - $incomplete_counts = $column_data['incomplete_request_counts']; - - $badges = ''; - - foreach ( $configured_languages as $lang ) { - if ( in_array( $lang, $cached_langs, true ) && isset( $incomplete_counts[ $lang ] ) ) { - $count = $incomplete_counts[ $lang ]; - $badges .= sprintf( - '%s %s', - esc_attr( - sprintf( - /* translators: %d: number of visitor requests that received only part of their texts from the cache */ - _n( '%d incomplete response', '%d incomplete responses', $count, 'yard-deepl' ), - $count - ) - ), - esc_html( $lang ), - esc_html( number_format_i18n( $count ) ) - ); - } elseif ( in_array( $lang, $cached_langs, true ) ) { - $badges .= sprintf( - '%s', - esc_html( $lang ) - ); - } elseif ( isset( $uncached_counts[ $lang ] ) ) { - $count = $uncached_counts[ $lang ]; - $badges .= sprintf( - '%s %s', - esc_attr( - sprintf( - /* translators: %d: number of uncached DeepL API calls made for this post and language */ - _n( '%d uncached call', '%d uncached calls', $count, 'yard-deepl' ), - $count - ) - ), - esc_html( $lang ), - esc_html( number_format_i18n( $count ) ) - ); - } - } - - if ( '' === $badges ) { - echo '—'; - - return; - } - - echo '
' . $badges . '
'; - } - - /** - * @since 2.0.0 - */ - public function enqueue_styles( string $hook_suffix ): void - { - if ( 'edit.php' !== $hook_suffix ) { - return; - } - - $screen = get_current_screen(); - - if ( ! $screen || ! in_array( $screen->post_type, $this->get_cache_metabox_post_types(), true ) ) { - return; - } - - $path = ydpl_asset_path( 'admin-columns.asset.php' ); - $script_asset = file_exists( $path ) ? require $path : array( - 'dependencies' => array(), - 'version' => round( microtime( true ) ), - ); - - wp_enqueue_style( 'ydpl-admin-columns', ydpl_asset_url( 'admin-columns.css' ), $script_asset['dependencies'] ?? array(), $script_asset['version'] ); - } -} diff --git a/src/Repositories/TranslationRepository.php b/src/Repositories/TranslationRepository.php index fdf16c6..af7be8c 100644 --- a/src/Repositories/TranslationRepository.php +++ b/src/Repositories/TranslationRepository.php @@ -43,99 +43,6 @@ public function get_cached_translation( int $object_id, string $target_lang ): ? return $cached_translation; } - /** - * Returns data needed to render the admin list-table column in a single meta fetch. - * - * 'cached_languages' — ISO codes from $language_codes whose cache is valid and non-stale. - * 'uncached_request_counts' — map of ISO code => cumulative uncached visitor API calls, only for - * languages that have at least one recorded call. - * 'incomplete_request_counts' — map of ISO code => visitor requests that received only part of - * their texts from the cache, only for languages with at least one. - * - * @since 2.0.0 - * - * @return array{ cached_languages: string[], uncached_request_counts: array, incomplete_request_counts: array } - */ - public function get_column_data( int $object_id, array $language_codes ): array - { - $empty = array( - 'cached_languages' => array(), - 'uncached_request_counts' => array(), - 'incomplete_request_counts' => array(), - ); - - if ( ! $this->translated_object_exists( $object_id ) || $this->is_cache_disabled( $object_id ) ) { - return $empty; - } - - $post_modified = get_post_field( 'post_modified', $object_id ); - $post_modified_timestamp = strtotime( $post_modified ); - $all_meta = get_post_meta( $object_id ); - $cached = array(); - $counts = array(); - $incomplete_counts = array(); - - foreach ( $language_codes as $lang ) { - $translation_value = $all_meta[ "_translation_$lang" ][0] ?? null; - $translation_modified = $all_meta[ "_translation_modified_$lang" ][0] ?? null; - - if ( $translation_value && $translation_modified && strtotime( $translation_modified ) >= $post_modified_timestamp ) { - $cached[] = $lang; - } - - $count = (int) ( $all_meta[ "_ydpl_uncached_request_count_$lang" ][0] ?? 0 ); - - if ( 0 < $count ) { - $counts[ $lang ] = $count; - } - - $incomplete_count = (int) ( $all_meta[ "_ydpl_incomplete_request_count_$lang" ][0] ?? 0 ); - - if ( 0 < $incomplete_count ) { - $incomplete_counts[ $lang ] = $incomplete_count; - } - } - - return array( - 'cached_languages' => $cached, - 'uncached_request_counts' => $counts, - 'incomplete_request_counts' => $incomplete_counts, - ); - } - - /** - * Increments the per-language count of uncached visitor API calls for this post. - * - * Only called for requests from visitors who lack cache-write capability, so the - * count reflects API spend that caching would have prevented. Returns silently - * when the object does not exist. - * - * @since 2.0.0 - */ - public function increment_uncached_request_count( int $object_id, string $target_lang ): void - { - $this->increment_request_count( $object_id, "_ydpl_uncached_request_count_$target_lang" ); - } - - /** - * @since 2.2.0 - */ - public function increment_incomplete_request_count( int $object_id, string $target_lang ): void - { - $this->increment_request_count( $object_id, "_ydpl_incomplete_request_count_$target_lang" ); - } - - protected function increment_request_count( int $object_id, string $key ): void - { - if ( ! $this->translated_object_exists( $object_id ) || $this->is_cache_disabled( $object_id ) ) { - return; - } - - $current = (int) get_post_meta( $object_id, $key, true ); - - update_post_meta( $object_id, $key, $current + 1 ); - } - /** * @since 0.0.1 */ @@ -158,7 +65,7 @@ public function delete_cached_translations( int $object_id ): void $meta_keys = array_keys( get_post_meta( $object_id ) ); foreach ( $meta_keys as $key ) { - if ( 0 === strpos( $key, '_translation_' ) || 0 === strpos( $key, '_ydpl_incomplete_request_count_' ) || 0 === strpos( $key, '_ydpl_uncached_request_count_' ) ) { + if ( 0 === strpos( $key, '_translation_' ) ) { delete_post_meta( $object_id, $key ); } } @@ -181,8 +88,6 @@ public function store_translation( int $object_id, string $target_lang, array $t update_post_meta( $object_id, "_translation_$target_lang", $translation ); update_post_meta( $object_id, "_translation_modified_$target_lang", current_time( 'mysql' ) ); - delete_post_meta( $object_id, "_ydpl_uncached_request_count_$target_lang" ); - delete_post_meta( $object_id, "_ydpl_incomplete_request_count_$target_lang" ); } protected function is_cache_disabled( int $object_id ): bool diff --git a/src/Services/TranslationService.php b/src/Services/TranslationService.php index c79902b..c65412a 100644 --- a/src/Services/TranslationService.php +++ b/src/Services/TranslationService.php @@ -64,8 +64,6 @@ public function handle_translation_with_object_id( int $object_id, array $text, if ( $cache ) { $this->repository->store_translation( $object_id, $target_lang, $merged ); - } else { - $this->repository->increment_uncached_request_count( $object_id, $target_lang ); } return $this->translations_for( $text, $merged ); @@ -118,14 +116,6 @@ public function handle_translation_without_object_id( array $text, string $targe return $translation; } - /** - * @since 2.2.0 - */ - public function increment_incomplete_request_count( int $object_id, string $target_lang ): void - { - $this->repository->increment_incomplete_request_count( $object_id, $target_lang ); - } - /** * @since 2.0.0 * diff --git a/src/Traits/CachePostTypesTrait.php b/src/Traits/CachePostTypesTrait.php index 1b48f1b..7c2e152 100644 --- a/src/Traits/CachePostTypesTrait.php +++ b/src/Traits/CachePostTypesTrait.php @@ -12,9 +12,6 @@ /** * Provides the list of post types that have DeepL cache support enabled. * - * Shared between MetaBoxServiceProvider and AdminColumnsServiceProvider so - * both always operate on the same set of post types. - * * @since 2.0.0 */ trait CachePostTypesTrait diff --git a/tests/Unit/AdminColumnsServiceProviderTest.php b/tests/Unit/AdminColumnsServiceProviderTest.php deleted file mode 100644 index 500c6e1..0000000 --- a/tests/Unit/AdminColumnsServiceProviderTest.php +++ /dev/null @@ -1,37 +0,0 @@ -render_column( 'ydpl_translation_cache', 1 ); - - return ob_get_clean(); -} - -it( - 'shows a cached language without incomplete responses as cached', - function () { - expect( rendered_column() )->toContain( 'ydpl-cache-badge--cached' ); - } -); - -it( - 'shows a cached language with incomplete responses as incomplete', - function () { - update_post_meta( 1, '_ydpl_incomplete_request_count_EN-GB', 2 ); - - expect( rendered_column() ) - ->toContain( 'ydpl-cache-badge--incomplete" title="2 incomplete responses">EN-GB' ) - ->not->toContain( 'ydpl-cache-badge--cached' ); - } -); diff --git a/tests/Unit/RestAPIControllerTest.php b/tests/Unit/RestAPIControllerTest.php index 295aa8f..a7c17c1 100644 --- a/tests/Unit/RestAPIControllerTest.php +++ b/tests/Unit/RestAPIControllerTest.php @@ -113,13 +113,3 @@ function () { expect( $response->status )->toBe( 200 ); } ); - -it( - 'counts visitor requests that received only part of their texts from the cache', - function () { - $this->controller->handle_translate_request( translate_request( array( 'Menu', 'Goedemiddag' ) ) ); - $this->controller->handle_translate_request( translate_request( array( 'Menu' ) ) ); - - expect( get_post_meta( 1, '_ydpl_incomplete_request_count_EN-GB', true ) )->toBe( 1 ); - } -); diff --git a/tests/Unit/TranslationRepositoryTest.php b/tests/Unit/TranslationRepositoryTest.php index a37ccfa..80bfeac 100644 --- a/tests/Unit/TranslationRepositoryTest.php +++ b/tests/Unit/TranslationRepositoryTest.php @@ -6,34 +6,13 @@ function () { given_post( 1, '2026-10-01 10:00:00' ); given_cached_translation( 1, 'EN-GB', array( pair( 'Menu', 'Menu EN' ) ), '2026-10-02 10:00:00' ); - update_post_meta( 1, '_ydpl_incomplete_request_count_EN-GB', 3 ); $this->repository = new TranslationRepository(); } ); it( - 'reports incomplete request counts per language', + 'deletes the cached translations', function () { - $column_data = $this->repository->get_column_data( 1, array( 'EN-GB', 'DE' ) ); - - expect( $column_data['incomplete_request_counts'] )->toBe( array( 'EN-GB' => 3 ) ); - } -); - -it( - 'resets the incomplete request count when a translation is stored', - function () { - $this->repository->store_translation( 1, 'EN-GB', array( pair( 'Menu', 'Menu EN' ) ) ); - - expect( get_post_meta( 1, '_ydpl_incomplete_request_count_EN-GB', true ) )->toBe( '' ); - } -); - -it( - 'deletes the request counts together with the cached translations', - function () { - update_post_meta( 1, '_ydpl_uncached_request_count_DE', 5 ); - $this->repository->delete_cached_translations( 1 ); expect( get_post_meta( 1 ) )->toBe( array() ); diff --git a/tests/Unit/TranslationServiceTest.php b/tests/Unit/TranslationServiceTest.php index 2104671..fdf3992 100644 --- a/tests/Unit/TranslationServiceTest.php +++ b/tests/Unit/TranslationServiceTest.php @@ -50,25 +50,22 @@ function () { 'merges new translations into the cache when the user may cache', function () { given_cached_translation( 1, 'EN-GB', array( pair( 'Menu', 'Menu EN' ) ), '2026-10-02 10:00:00' ); - update_post_meta( 1, '_ydpl_uncached_request_count_EN-GB', 4 ); $this->service->handle_translation_with_object_id( 1, array( 'Nieuws' ), 'EN-GB', true ); expect( get_post_meta( 1, '_translation_EN-GB', true ) ) - ->toEqualCanonicalizing( array( pair( 'Nieuws', 'EN-GB:Nieuws' ), pair( 'Menu', 'Menu EN' ) ) ) - ->and( get_post_meta( 1, '_ydpl_uncached_request_count_EN-GB', true ) )->toBe( '' ); + ->toEqualCanonicalizing( array( pair( 'Nieuws', 'EN-GB:Nieuws' ), pair( 'Menu', 'Menu EN' ) ) ); } ); it( - 'keeps the cache untouched and counts the request when the user may not cache', + 'keeps the cache untouched when the user may not cache', function () { given_cached_translation( 1, 'EN-GB', array( pair( 'Menu', 'Menu EN' ) ), '2026-10-02 10:00:00' ); $this->service->handle_translation_with_object_id( 1, array( 'Nieuws' ), 'EN-GB' ); - expect( get_post_meta( 1, '_translation_EN-GB', true ) )->toBe( array( pair( 'Menu', 'Menu EN' ) ) ) - ->and( get_post_meta( 1, '_ydpl_uncached_request_count_EN-GB', true ) )->toBe( 1 ); + expect( get_post_meta( 1, '_translation_EN-GB', true ) )->toBe( array( pair( 'Menu', 'Menu EN' ) ) ); } ); diff --git a/webpack.config.js b/webpack.config.js index d3d6ddc..19ef71a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,7 +5,6 @@ const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); // module.exports = { ...defaultConfig, entry: { - 'admin-columns': [ './assets/css/admin-columns.css' ], editor: [ './assets/css/editor.css' ], main: [ './assets/js/translation.js' ], }, From ae60224e6c74a71c1e8ea4281ec32466177439ce Mon Sep 17 00:00:00 2001 From: YvetteNikolov Date: Tue, 10 Nov 2026 16:34:01 +0100 Subject: [PATCH 2/3] feat: make translation cache column opt-in and more compact Restores the column and its counters, but only registers it when enabled on the settings page. Shortens the label to "Cache" and narrows the column to 6%. --- README.md | 11 ++ assets/css/admin-columns.css | 46 +++++ dist/admin-columns.asset.php | 1 + dist/admin-columns.css | 1 + dist/admin-columns.js | 0 languages/yard-deepl-nl_NL.mo | Bin 3631 -> 4062 bytes languages/yard-deepl-nl_NL.po | 82 +++++---- languages/yard-deepl.pot | 80 +++++---- readme.txt | 11 ++ src/Bootstrap.php | 2 + src/Controllers/RestAPIController.php | 4 + src/Controllers/SettingsController.php | 1 + src/Providers/AdminColumnsServiceProvider.php | 166 ++++++++++++++++++ src/Providers/SettingsServiceProvider.php | 9 + src/Repositories/TranslationRepository.php | 97 +++++++++- src/Services/TranslationService.php | 10 ++ src/Singletons/SiteOptionsSingleton.php | 8 + src/Traits/CachePostTypesTrait.php | 3 + .../partials/settings/settings-fields.php | 8 + .../Unit/AdminColumnsServiceProviderTest.php | 37 ++++ tests/Unit/RestAPIControllerTest.php | 10 ++ tests/Unit/TranslationRepositoryTest.php | 23 ++- tests/Unit/TranslationServiceTest.php | 9 +- webpack.config.js | 1 + 24 files changed, 544 insertions(+), 76 deletions(-) create mode 100644 assets/css/admin-columns.css create mode 100644 dist/admin-columns.asset.php create mode 100644 dist/admin-columns.css create mode 100644 dist/admin-columns.js create mode 100644 src/Providers/AdminColumnsServiceProvider.php create mode 100644 tests/Unit/AdminColumnsServiceProviderTest.php diff --git a/README.md b/README.md index 2414444..6a7c4b3 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,17 @@ A metabox labeled **Yard DeepL** is displayed on the edit screen of supported po - **Disable translation cache:** When checked, the cache is bypassed for this object. Useful for posts with dynamic content that should always be translated fresh. - **Clear translation cache:** When checked and the post is saved, all cached translations for this object are deleted. +### Admin: Translation Cache Column + +A **Cache** column can be added to the post list screen of all supported post types. It is off by default; enable it under **Settings > Yard DeepL > Translation cache column**. Its purpose is to notify editors which posts should be cached as soon as possible to avoid unnecessary DeepL API costs. + +- **Green badge per language** (e.g. `NL`, `EN-US`): a valid, up-to-date cached translation exists for that language. Visitor requests are served from cache at no cost. +- **Yellow badge with a count per language** (e.g. `EN-US 3`): a cached translation exists, but that many visitor requests received only part of their texts because the rest is missing from the cache. Hovering the badge shows the full count as a tooltip. An editor visiting the page translates the missing texts, stores them and resets the count. +- **Grey badge with a count per language** (e.g. `EN-US 42`): no cache exists for that language and visitors have triggered that many live DeepL API calls. Hovering the badge shows the full count as a tooltip. These posts are the most urgent to cache. +- **—**: the post has never been requested for translation, or caching is disabled for this post. + +The uncached call count is only incremented for visitor requests (users without cache-write capability) and is automatically reset per language once a cache entry is stored. The same applies to the incomplete response count, which is also removed when the cache is cleared. Posts with the **Disable translation cache** option enabled are excluded from the column entirely. + ## External Services This plugin connects to the DeepL API to provide translations for content. diff --git a/assets/css/admin-columns.css b/assets/css/admin-columns.css new file mode 100644 index 0000000..4c89aba --- /dev/null +++ b/assets/css/admin-columns.css @@ -0,0 +1,46 @@ +.fixed .column-ydpl_translation_cache { + width: 6%; +} + +.ydpl-cache-badges { + display: flex; + flex-wrap: wrap; + gap: 2px; +} + +.ydpl-cache-badge { + display: inline-flex; + align-items: center; + gap: 3px; + font-size: 9px; + line-height: 1; + padding: 2px 4px; + border-radius: 3px; + font-weight: 600; + letter-spacing: 0.03em; +} + +.ydpl-cache-badge--cached { + background: #00a32a; + color: #fff; +} + +.ydpl-cache-badge--incomplete { + background: #dba617; + color: #1d2327; +} + +.ydpl-cache-badge--uncached { + background: #f0f0f1; + color: #3c434a; +} + +.ydpl-badge-count { + font-weight: 400; + font-size: 9px; + opacity: 0.7; +} + +.ydpl-cache-status--none { + color: #8c8f94; +} diff --git a/dist/admin-columns.asset.php b/dist/admin-columns.asset.php new file mode 100644 index 0000000..340bc6a --- /dev/null +++ b/dist/admin-columns.asset.php @@ -0,0 +1 @@ + array(), 'version' => '48a94204d54696f323e5'); diff --git a/dist/admin-columns.css b/dist/admin-columns.css new file mode 100644 index 0000000..bb96c92 --- /dev/null +++ b/dist/admin-columns.css @@ -0,0 +1 @@ +.fixed .column-ydpl_translation_cache{width:6%}.ydpl-cache-badges{display:flex;flex-wrap:wrap;gap:2px}.ydpl-cache-badge{align-items:center;border-radius:3px;display:inline-flex;font-size:9px;font-weight:600;gap:3px;letter-spacing:.03em;line-height:1;padding:2px 4px}.ydpl-cache-badge--cached{background:#00a32a;color:#fff}.ydpl-cache-badge--incomplete{background:#dba617;color:#1d2327}.ydpl-cache-badge--uncached{background:#f0f0f1;color:#3c434a}.ydpl-badge-count{font-size:9px;font-weight:400;opacity:.7}.ydpl-cache-status--none{color:#8c8f94} diff --git a/dist/admin-columns.js b/dist/admin-columns.js new file mode 100644 index 0000000..e69de29 diff --git a/languages/yard-deepl-nl_NL.mo b/languages/yard-deepl-nl_NL.mo index 26d7fae5dc8b8b40f77f4585e8f223a93882fc66..e9f79a217a6d7bfa9a594ac8ff65a7285fa151c1 100644 GIT binary patch delta 1482 zcmYk+OKeP09LMo9ZFLN7JxfvCsz-}qW~kDTh;)O+NHrE7iEG?UC&t|BjE+Ziwkq*R z>X8V7Af7>nbm5VZ2x4JDYC~foLE;q))Q0b`y%Hz)e?Iqd?mg%H&zZSi**iJcJF@7A zp)?Sc#3!FIQM_A7Mfp`^%pB~)DOfYem||Rs0c=7Jvz}@+Zo+cRqT27r3cP_F<|&ol ze~u%K$(bHbO1be7$Kg+G#1XU_k86-yW*hQ?$>IpyhlBA1`tdw+m}{Q5Q19JA4fFvH z!Dp!UJvf;DO)n=L=Dqje3##D{9D_bukHrd9$03}E%TVuaM74{fX5N7s*kR-_C#guD zIq%)SjPtq9V=?`k51eR(Uy;M~dHVQDit7M&;tpJk1vFMlH=x?LBZujr(!lqk2GWJ2 z@Di%sJuG8+A7O~=mo(0WIT`Yfu$ZdCh+s1Bc? zGW8lY&@Z?G{UrmLYeK!(T0;Ky!ftMq;X%|$PoYwF0rlZ4o;Oh`??$EgKJsnz7@3rL z;XQxjJ%5M#&S%tjzoYilZ`58ZFD3uld~>O^rb|#Au0ze-M$L35s>1`G$50C4WI{E z(*B}N^{;5q`Z;URYNza|(7b=Z^H9I=++-@1g@k@ybBP6nmSYB?^r~nuD(YONp0EgI zX#$}#oKP136(((h-i7v$ivDM`C#ncClQXk9nL|t`RB8y#ROwXF`Y9t*32i?8si@Qu zlZa`$pv)q)t80k@B0|h0w9Z;{ZOH%2qW+7nAbC?n1zi+obh3`U4= z7uCQkID@mefE#!Yukug{)KT+RskFg5D&Z|u0^hL>V`M?oII!1@jk}5f1P}b0i86s#lxVb-0jl&{TX z`X?)HzBA|Kj%A$VnIqXz$H_NJx=}}q!\n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-07-31T00:00:00+00:00\n" +"POT-Creation-Date: 2026-11-10T15:32:27+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: yard-deepl\n" #. Plugin Name of the plugin #: yard-deepl.php:8 -#: src/Providers/MetaBoxServiceProvider.php:35 +#: src/Providers/MetaBoxServiceProvider.php:38 #: src/Providers/SettingsServiceProvider.php:44 #: src/Providers/SettingsServiceProvider.php:45 msgid "Yard DeepL" @@ -169,19 +169,51 @@ msgstr "" msgid "Chinese (Traditional)" msgstr "" -#: src/Providers/MetaBoxServiceProvider.php:72 -msgid "Disable translation cache when this object contains dynamic content." +#: src/Controllers/SettingsController.php:116 +msgid "The DeepL API base URL must be a valid https://*.deepl.com/v2/translate URL. The previous value has been kept." +msgstr "" + +#: src/Providers/AdminColumnsServiceProvider.php:68 +msgid "Cache" +msgstr "" + +#: src/Providers/AdminColumnsServiceProvider.php:85 +msgid "No languages configured" +msgstr "" + +#. translators: %d: number of visitor requests that received only part of their texts from the cache +#: src/Providers/AdminColumnsServiceProvider.php:105 +#, php-format +msgid "%d incomplete response" +msgid_plural "%d incomplete responses" +msgstr[0] "" +msgstr[1] "" + +#. translators: %d: number of uncached DeepL API calls made for this post and language +#: src/Providers/AdminColumnsServiceProvider.php:124 +#, php-format +msgid "%d uncached call" +msgid_plural "%d uncached calls" +msgstr[0] "" +msgstr[1] "" + +#: src/Providers/AdminColumnsServiceProvider.php:135 +msgid "No cached translations" msgstr "" #: src/Providers/MetaBoxServiceProvider.php:75 -msgid "Disable translation cache?" +msgid "Disable translation cache when this object contains dynamic content." msgstr "" #: src/Providers/MetaBoxServiceProvider.php:78 -msgid "Clear cached translations on save." +msgid "Disable translation cache?" msgstr "" #: src/Providers/MetaBoxServiceProvider.php:81 +msgid "Clear cached translations on save." +msgstr "" + +#: src/Providers/MetaBoxServiceProvider.php:84 msgid "Clear translation cache?" msgstr "" @@ -201,12 +233,12 @@ msgstr "" msgid "DeepL supported languages" msgstr "" -#: src/Providers/SettingsServiceProvider.php:110 -msgid "Parameter object_id mandatory" +#: src/Providers/SettingsServiceProvider.php:103 +msgid "Translation cache column" msgstr "" -#: src/Controllers/SettingsController.php:112 -msgid "The DeepL API base URL must be a valid https://*.deepl.com/v2/translate URL. The previous value has been kept." +#: src/Providers/SettingsServiceProvider.php:119 +msgid "Parameter object_id mandatory" msgstr "" #. Translators: %s is the URL to the DeepL API documentation page. @@ -220,32 +252,10 @@ msgstr "" msgid "To query the DeepL API, an API key is required. You can find more information on their %s." msgstr "" -#: src/Providers/AdminColumnsServiceProvider.php:53 -msgid "Translation cache" -msgstr "" - -#: src/Providers/AdminColumnsServiceProvider.php:70 -msgid "No languages configured" +#: src/Views/admin/partials/settings/settings-fields.php:44 +msgid "Show which languages are cached on post list screens" msgstr "" -#: src/Providers/AdminColumnsServiceProvider.php:105 -msgid "No cached translations" -msgstr "" - -#. Translators: %d is the number of visitor requests that received only part of their texts from the cache. -#: src/Providers/AdminColumnsServiceProvider.php:90 -msgid "%d incomplete response" -msgid_plural "%d incomplete responses" -msgstr[0] "" -msgstr[1] "" - -#. Translators: %d is the number of uncached DeepL API calls made for this post and language. -#: src/Providers/AdminColumnsServiceProvider.php:109 -msgid "%d uncached call" -msgid_plural "%d uncached calls" -msgstr[0] "" -msgstr[1] "" - #: src/Views/admin/settings-page.php:15 msgid "Save" msgstr "" diff --git a/readme.txt b/readme.txt index 3afee22..9190170 100644 --- a/readme.txt +++ b/readme.txt @@ -33,6 +33,17 @@ A metabox labeled Yard DeepL is displayed on the edit screen of supported post t * Disable translation cache: When checked, the cache is bypassed for this object. Useful for posts with dynamic content that should always be translated fresh. * Clear translation cache: When checked and the post is saved, all cached translations for this object are deleted. += Admin: Translation Cache Column = + +A Cache column can be added to the post list screen of all supported post types. It is off by default; enable it under Settings > Yard DeepL > Translation cache column. Its purpose is to notify editors which posts should be cached as soon as possible to avoid unnecessary DeepL API costs. + +* Green badge per language (e.g. NL, EN-US): a valid, up-to-date cached translation exists for that language. Visitor requests are served from cache at no cost. +* Yellow badge with a count per language (e.g. EN-US 3): a cached translation exists, but that many visitor requests received only part of their texts because the rest is missing from the cache. Hovering the badge shows the full count as a tooltip. An editor visiting the page translates the missing texts, stores them and resets the count. +* Grey badge with a count per language (e.g. EN-US 42): no cache exists for that language and visitors have triggered that many live DeepL API calls. Hovering the badge shows the full count as a tooltip. These posts are the most urgent to cache. +* —: the post has never been requested for translation, or caching is disabled for this post. + +The uncached call count is only incremented for visitor requests (users without cache-write capability) and is automatically reset per language once a cache entry is stored. The same applies to the incomplete response count, which is also removed when the cache is cleared. Posts with the Disable translation cache option enabled are excluded from the column entirely. + == External Services == This plugin connects to the DeepL API to provide translations for content. diff --git a/src/Bootstrap.php b/src/Bootstrap.php index 20abea2..1c0cfaf 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -9,6 +9,7 @@ exit; } +use YDPL\Providers\AdminColumnsServiceProvider; use YDPL\Providers\AssetsServiceProvider; use YDPL\Providers\MetaBoxServiceProvider; use YDPL\Providers\RestAPIServiceProvider; @@ -71,6 +72,7 @@ protected function get_providers(): array new SettingsServiceProvider(), new RestAPIServiceProvider(), new MetaBoxServiceProvider(), + new AdminColumnsServiceProvider(), ); } diff --git a/src/Controllers/RestAPIController.php b/src/Controllers/RestAPIController.php index 67d4d9e..a615663 100644 --- a/src/Controllers/RestAPIController.php +++ b/src/Controllers/RestAPIController.php @@ -68,6 +68,10 @@ public function handle_translate_request( WP_REST_Request $request ): WP_REST_Re if ( ! $user_has_cache_capability && array() !== $missing_texts ) { $cached_texts = $this->service->translations_for( $text, $cached_translation ?? array() ); + if ( array() !== $cached_texts ) { + $this->service->increment_incomplete_request_count( $object_id, $target_lang ); + } + if ( $this->is_rate_limit_exceeded() ) { return array() !== $cached_texts ? new WP_REST_Response( $cached_texts ) diff --git a/src/Controllers/SettingsController.php b/src/Controllers/SettingsController.php index db65207..0e22afd 100644 --- a/src/Controllers/SettingsController.php +++ b/src/Controllers/SettingsController.php @@ -55,6 +55,7 @@ public function section_fields_render( array $args ): void 'supported_languages' => ydpl_resolve_from_container( 'ydpl.supported_target.languages' ), 'configured_supported_languages' => ydpl_resolve_from_container( 'ydpl.site_options' )->configured_supported_languages(), 'rest_api_param_object_id_is_mandatory' => ydpl_resolve_from_container( 'ydpl.site_options' )->rest_api_param_object_id_is_mandatory(), + 'admin_column_enabled' => ydpl_resolve_from_container( 'ydpl.site_options' )->admin_column_enabled(), ) ); } diff --git a/src/Providers/AdminColumnsServiceProvider.php b/src/Providers/AdminColumnsServiceProvider.php new file mode 100644 index 0000000..f3d0375 --- /dev/null +++ b/src/Providers/AdminColumnsServiceProvider.php @@ -0,0 +1,166 @@ +repository = new TranslationRepository(); + } + + /** + * @since 2.0.0 + */ + public function register(): void + { + if ( ! ydpl_resolve_from_container( 'ydpl.site_options' )->admin_column_enabled() ) { + return; + } + + add_action( 'admin_init', array( $this, 'register_columns' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) ); + } + + /** + * Must run after every post type is registered on init. + * + * @since 2.2.0 + */ + public function register_columns(): void + { + foreach ( $this->get_cache_metabox_post_types() as $post_type ) { + add_filter( "manage_{$post_type}_posts_columns", array( $this, 'add_column' ) ); + add_action( "manage_{$post_type}_posts_custom_column", array( $this, 'render_column' ), 10, 2 ); + } + } + + /** + * @since 2.0.0 + */ + public function add_column( array $columns ): array + { + $columns['ydpl_translation_cache'] = __( 'Cache', 'yard-deepl' ); + + return $columns; + } + + /** + * @since 2.0.0 + */ + public function render_column( string $column, int $post_id ): void + { + if ( 'ydpl_translation_cache' !== $column ) { + return; + } + + $configured_languages = ydpl_resolve_from_container( 'ydpl.site_options' )->configured_supported_languages(); + + if ( array() === $configured_languages ) { + echo '—'; + + return; + } + + $column_data = $this->repository->get_column_data( $post_id, $configured_languages ); + $cached_langs = $column_data['cached_languages']; + $uncached_counts = $column_data['uncached_request_counts']; + $incomplete_counts = $column_data['incomplete_request_counts']; + + $badges = ''; + + foreach ( $configured_languages as $lang ) { + if ( in_array( $lang, $cached_langs, true ) && isset( $incomplete_counts[ $lang ] ) ) { + $count = $incomplete_counts[ $lang ]; + $badges .= sprintf( + '%s %s', + esc_attr( + sprintf( + /* translators: %d: number of visitor requests that received only part of their texts from the cache */ + _n( '%d incomplete response', '%d incomplete responses', $count, 'yard-deepl' ), + $count + ) + ), + esc_html( $lang ), + esc_html( number_format_i18n( $count ) ) + ); + } elseif ( in_array( $lang, $cached_langs, true ) ) { + $badges .= sprintf( + '%s', + esc_html( $lang ) + ); + } elseif ( isset( $uncached_counts[ $lang ] ) ) { + $count = $uncached_counts[ $lang ]; + $badges .= sprintf( + '%s %s', + esc_attr( + sprintf( + /* translators: %d: number of uncached DeepL API calls made for this post and language */ + _n( '%d uncached call', '%d uncached calls', $count, 'yard-deepl' ), + $count + ) + ), + esc_html( $lang ), + esc_html( number_format_i18n( $count ) ) + ); + } + } + + if ( '' === $badges ) { + echo '—'; + + return; + } + + echo '
' . $badges . '
'; + } + + /** + * @since 2.0.0 + */ + public function enqueue_styles( string $hook_suffix ): void + { + if ( 'edit.php' !== $hook_suffix ) { + return; + } + + $screen = get_current_screen(); + + if ( ! $screen || ! in_array( $screen->post_type, $this->get_cache_metabox_post_types(), true ) ) { + return; + } + + $path = ydpl_asset_path( 'admin-columns.asset.php' ); + $script_asset = file_exists( $path ) ? require $path : array( + 'dependencies' => array(), + 'version' => round( microtime( true ) ), + ); + + wp_enqueue_style( 'ydpl-admin-columns', ydpl_asset_url( 'admin-columns.css' ), $script_asset['dependencies'] ?? array(), $script_asset['version'] ); + } +} diff --git a/src/Providers/SettingsServiceProvider.php b/src/Providers/SettingsServiceProvider.php index 2c989d6..ac14d34 100644 --- a/src/Providers/SettingsServiceProvider.php +++ b/src/Providers/SettingsServiceProvider.php @@ -98,6 +98,15 @@ public function register_settings_options(): void array( 'settings_field_id' => 'ydpl_supported_target_languages' ) ); + add_settings_field( + 'ydpl_admin_column_enabled', + __( 'Translation cache column', 'yard-deepl' ), + array( $this->controller, 'section_fields_render' ), + 'yard-deepl', + 'ydpl_section_general', + array( 'settings_field_id' => 'ydpl_admin_column_enabled' ) + ); + add_settings_section( 'ydpl_section_rest_api', 'REST API', diff --git a/src/Repositories/TranslationRepository.php b/src/Repositories/TranslationRepository.php index af7be8c..fdf16c6 100644 --- a/src/Repositories/TranslationRepository.php +++ b/src/Repositories/TranslationRepository.php @@ -43,6 +43,99 @@ public function get_cached_translation( int $object_id, string $target_lang ): ? return $cached_translation; } + /** + * Returns data needed to render the admin list-table column in a single meta fetch. + * + * 'cached_languages' — ISO codes from $language_codes whose cache is valid and non-stale. + * 'uncached_request_counts' — map of ISO code => cumulative uncached visitor API calls, only for + * languages that have at least one recorded call. + * 'incomplete_request_counts' — map of ISO code => visitor requests that received only part of + * their texts from the cache, only for languages with at least one. + * + * @since 2.0.0 + * + * @return array{ cached_languages: string[], uncached_request_counts: array, incomplete_request_counts: array } + */ + public function get_column_data( int $object_id, array $language_codes ): array + { + $empty = array( + 'cached_languages' => array(), + 'uncached_request_counts' => array(), + 'incomplete_request_counts' => array(), + ); + + if ( ! $this->translated_object_exists( $object_id ) || $this->is_cache_disabled( $object_id ) ) { + return $empty; + } + + $post_modified = get_post_field( 'post_modified', $object_id ); + $post_modified_timestamp = strtotime( $post_modified ); + $all_meta = get_post_meta( $object_id ); + $cached = array(); + $counts = array(); + $incomplete_counts = array(); + + foreach ( $language_codes as $lang ) { + $translation_value = $all_meta[ "_translation_$lang" ][0] ?? null; + $translation_modified = $all_meta[ "_translation_modified_$lang" ][0] ?? null; + + if ( $translation_value && $translation_modified && strtotime( $translation_modified ) >= $post_modified_timestamp ) { + $cached[] = $lang; + } + + $count = (int) ( $all_meta[ "_ydpl_uncached_request_count_$lang" ][0] ?? 0 ); + + if ( 0 < $count ) { + $counts[ $lang ] = $count; + } + + $incomplete_count = (int) ( $all_meta[ "_ydpl_incomplete_request_count_$lang" ][0] ?? 0 ); + + if ( 0 < $incomplete_count ) { + $incomplete_counts[ $lang ] = $incomplete_count; + } + } + + return array( + 'cached_languages' => $cached, + 'uncached_request_counts' => $counts, + 'incomplete_request_counts' => $incomplete_counts, + ); + } + + /** + * Increments the per-language count of uncached visitor API calls for this post. + * + * Only called for requests from visitors who lack cache-write capability, so the + * count reflects API spend that caching would have prevented. Returns silently + * when the object does not exist. + * + * @since 2.0.0 + */ + public function increment_uncached_request_count( int $object_id, string $target_lang ): void + { + $this->increment_request_count( $object_id, "_ydpl_uncached_request_count_$target_lang" ); + } + + /** + * @since 2.2.0 + */ + public function increment_incomplete_request_count( int $object_id, string $target_lang ): void + { + $this->increment_request_count( $object_id, "_ydpl_incomplete_request_count_$target_lang" ); + } + + protected function increment_request_count( int $object_id, string $key ): void + { + if ( ! $this->translated_object_exists( $object_id ) || $this->is_cache_disabled( $object_id ) ) { + return; + } + + $current = (int) get_post_meta( $object_id, $key, true ); + + update_post_meta( $object_id, $key, $current + 1 ); + } + /** * @since 0.0.1 */ @@ -65,7 +158,7 @@ public function delete_cached_translations( int $object_id ): void $meta_keys = array_keys( get_post_meta( $object_id ) ); foreach ( $meta_keys as $key ) { - if ( 0 === strpos( $key, '_translation_' ) ) { + if ( 0 === strpos( $key, '_translation_' ) || 0 === strpos( $key, '_ydpl_incomplete_request_count_' ) || 0 === strpos( $key, '_ydpl_uncached_request_count_' ) ) { delete_post_meta( $object_id, $key ); } } @@ -88,6 +181,8 @@ public function store_translation( int $object_id, string $target_lang, array $t update_post_meta( $object_id, "_translation_$target_lang", $translation ); update_post_meta( $object_id, "_translation_modified_$target_lang", current_time( 'mysql' ) ); + delete_post_meta( $object_id, "_ydpl_uncached_request_count_$target_lang" ); + delete_post_meta( $object_id, "_ydpl_incomplete_request_count_$target_lang" ); } protected function is_cache_disabled( int $object_id ): bool diff --git a/src/Services/TranslationService.php b/src/Services/TranslationService.php index c65412a..c79902b 100644 --- a/src/Services/TranslationService.php +++ b/src/Services/TranslationService.php @@ -64,6 +64,8 @@ public function handle_translation_with_object_id( int $object_id, array $text, if ( $cache ) { $this->repository->store_translation( $object_id, $target_lang, $merged ); + } else { + $this->repository->increment_uncached_request_count( $object_id, $target_lang ); } return $this->translations_for( $text, $merged ); @@ -116,6 +118,14 @@ public function handle_translation_without_object_id( array $text, string $targe return $translation; } + /** + * @since 2.2.0 + */ + public function increment_incomplete_request_count( int $object_id, string $target_lang ): void + { + $this->repository->increment_incomplete_request_count( $object_id, $target_lang ); + } + /** * @since 2.0.0 * diff --git a/src/Singletons/SiteOptionsSingleton.php b/src/Singletons/SiteOptionsSingleton.php index e994e6a..6fba584 100644 --- a/src/Singletons/SiteOptionsSingleton.php +++ b/src/Singletons/SiteOptionsSingleton.php @@ -87,4 +87,12 @@ public function rest_api_param_object_id_is_mandatory(): bool return 'on' === $value; } + + /** + * @since 2.3.0 + */ + public function admin_column_enabled(): bool + { + return 'on' === ( $this->options['ydpl_admin_column_enabled'] ?? '' ); + } } diff --git a/src/Traits/CachePostTypesTrait.php b/src/Traits/CachePostTypesTrait.php index 7c2e152..1b48f1b 100644 --- a/src/Traits/CachePostTypesTrait.php +++ b/src/Traits/CachePostTypesTrait.php @@ -12,6 +12,9 @@ /** * Provides the list of post types that have DeepL cache support enabled. * + * Shared between MetaBoxServiceProvider and AdminColumnsServiceProvider so + * both always operate on the same set of post types. + * * @since 2.0.0 */ trait CachePostTypesTrait diff --git a/src/Views/admin/partials/settings/settings-fields.php b/src/Views/admin/partials/settings/settings-fields.php index 4cff557..18ed3d7 100644 --- a/src/Views/admin/partials/settings/settings-fields.php +++ b/src/Views/admin/partials/settings/settings-fields.php @@ -13,6 +13,7 @@ $supported_languages = is_array( $supported_languages ?? null ) ? $supported_languages : array(); $configured_supported_languages = is_array( $configured_supported_languages ?? null ) ? $configured_supported_languages : array(); $rest_api_param_object_id_is_mandatory = $rest_api_param_object_id_is_mandatory ?? true; +$admin_column_enabled = $admin_column_enabled ?? false; ?> @@ -36,3 +37,10 @@ > + + + + diff --git a/tests/Unit/AdminColumnsServiceProviderTest.php b/tests/Unit/AdminColumnsServiceProviderTest.php new file mode 100644 index 0000000..500c6e1 --- /dev/null +++ b/tests/Unit/AdminColumnsServiceProviderTest.php @@ -0,0 +1,37 @@ +render_column( 'ydpl_translation_cache', 1 ); + + return ob_get_clean(); +} + +it( + 'shows a cached language without incomplete responses as cached', + function () { + expect( rendered_column() )->toContain( 'ydpl-cache-badge--cached' ); + } +); + +it( + 'shows a cached language with incomplete responses as incomplete', + function () { + update_post_meta( 1, '_ydpl_incomplete_request_count_EN-GB', 2 ); + + expect( rendered_column() ) + ->toContain( 'ydpl-cache-badge--incomplete" title="2 incomplete responses">EN-GB' ) + ->not->toContain( 'ydpl-cache-badge--cached' ); + } +); diff --git a/tests/Unit/RestAPIControllerTest.php b/tests/Unit/RestAPIControllerTest.php index a7c17c1..295aa8f 100644 --- a/tests/Unit/RestAPIControllerTest.php +++ b/tests/Unit/RestAPIControllerTest.php @@ -113,3 +113,13 @@ function () { expect( $response->status )->toBe( 200 ); } ); + +it( + 'counts visitor requests that received only part of their texts from the cache', + function () { + $this->controller->handle_translate_request( translate_request( array( 'Menu', 'Goedemiddag' ) ) ); + $this->controller->handle_translate_request( translate_request( array( 'Menu' ) ) ); + + expect( get_post_meta( 1, '_ydpl_incomplete_request_count_EN-GB', true ) )->toBe( 1 ); + } +); diff --git a/tests/Unit/TranslationRepositoryTest.php b/tests/Unit/TranslationRepositoryTest.php index 80bfeac..a37ccfa 100644 --- a/tests/Unit/TranslationRepositoryTest.php +++ b/tests/Unit/TranslationRepositoryTest.php @@ -6,13 +6,34 @@ function () { given_post( 1, '2026-10-01 10:00:00' ); given_cached_translation( 1, 'EN-GB', array( pair( 'Menu', 'Menu EN' ) ), '2026-10-02 10:00:00' ); + update_post_meta( 1, '_ydpl_incomplete_request_count_EN-GB', 3 ); $this->repository = new TranslationRepository(); } ); it( - 'deletes the cached translations', + 'reports incomplete request counts per language', function () { + $column_data = $this->repository->get_column_data( 1, array( 'EN-GB', 'DE' ) ); + + expect( $column_data['incomplete_request_counts'] )->toBe( array( 'EN-GB' => 3 ) ); + } +); + +it( + 'resets the incomplete request count when a translation is stored', + function () { + $this->repository->store_translation( 1, 'EN-GB', array( pair( 'Menu', 'Menu EN' ) ) ); + + expect( get_post_meta( 1, '_ydpl_incomplete_request_count_EN-GB', true ) )->toBe( '' ); + } +); + +it( + 'deletes the request counts together with the cached translations', + function () { + update_post_meta( 1, '_ydpl_uncached_request_count_DE', 5 ); + $this->repository->delete_cached_translations( 1 ); expect( get_post_meta( 1 ) )->toBe( array() ); diff --git a/tests/Unit/TranslationServiceTest.php b/tests/Unit/TranslationServiceTest.php index fdf3992..2104671 100644 --- a/tests/Unit/TranslationServiceTest.php +++ b/tests/Unit/TranslationServiceTest.php @@ -50,22 +50,25 @@ function () { 'merges new translations into the cache when the user may cache', function () { given_cached_translation( 1, 'EN-GB', array( pair( 'Menu', 'Menu EN' ) ), '2026-10-02 10:00:00' ); + update_post_meta( 1, '_ydpl_uncached_request_count_EN-GB', 4 ); $this->service->handle_translation_with_object_id( 1, array( 'Nieuws' ), 'EN-GB', true ); expect( get_post_meta( 1, '_translation_EN-GB', true ) ) - ->toEqualCanonicalizing( array( pair( 'Nieuws', 'EN-GB:Nieuws' ), pair( 'Menu', 'Menu EN' ) ) ); + ->toEqualCanonicalizing( array( pair( 'Nieuws', 'EN-GB:Nieuws' ), pair( 'Menu', 'Menu EN' ) ) ) + ->and( get_post_meta( 1, '_ydpl_uncached_request_count_EN-GB', true ) )->toBe( '' ); } ); it( - 'keeps the cache untouched when the user may not cache', + 'keeps the cache untouched and counts the request when the user may not cache', function () { given_cached_translation( 1, 'EN-GB', array( pair( 'Menu', 'Menu EN' ) ), '2026-10-02 10:00:00' ); $this->service->handle_translation_with_object_id( 1, array( 'Nieuws' ), 'EN-GB' ); - expect( get_post_meta( 1, '_translation_EN-GB', true ) )->toBe( array( pair( 'Menu', 'Menu EN' ) ) ); + expect( get_post_meta( 1, '_translation_EN-GB', true ) )->toBe( array( pair( 'Menu', 'Menu EN' ) ) ) + ->and( get_post_meta( 1, '_ydpl_uncached_request_count_EN-GB', true ) )->toBe( 1 ); } ); diff --git a/webpack.config.js b/webpack.config.js index 19ef71a..d3d6ddc 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,6 +5,7 @@ const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); // module.exports = { ...defaultConfig, entry: { + 'admin-columns': [ './assets/css/admin-columns.css' ], editor: [ './assets/css/editor.css' ], main: [ './assets/js/translation.js' ], }, From 88066bd804c8961456fbc28d34509ad78941d87b Mon Sep 17 00:00:00 2001 From: YvetteNikolov Date: Thu, 12 Nov 2026 12:08:23 +0100 Subject: [PATCH 3/3] docs: use @since NEXT for translation cache column option --- src/Singletons/SiteOptionsSingleton.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Singletons/SiteOptionsSingleton.php b/src/Singletons/SiteOptionsSingleton.php index 6fba584..ed99bea 100644 --- a/src/Singletons/SiteOptionsSingleton.php +++ b/src/Singletons/SiteOptionsSingleton.php @@ -89,7 +89,7 @@ public function rest_api_param_object_id_is_mandatory(): bool } /** - * @since 2.3.0 + * @since NEXT */ public function admin_column_enabled(): bool {