Skip to content

fix: cache DeepL translations per text and translate missing texts - #8

Merged
SimonvanWijhe merged 8 commits into
mainfrom
fix/deepl-cache-per-text
Nov 5, 2026
Merged

SimonvanWijhe merged 8 commits into
mainfrom
fix/deepl-cache-per-text

Conversation

@SimonvanWijhe

@SimonvanWijhe SimonvanWijhe commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

De cache bewaarde de complete respons per pagina en gaf die terug ongeacht welke teksten de client stuurde. Menu, footer, nieuwslijst of groet die na het cachen wijzigen werden daardoor nooit vertaald, en admin-bar-teksten van de editor die de cache vulde kwamen mee in de respons voor bezoekers. Op geertruidenberg.nl ontbraken zo 9 teksten op de EN-GB homepage. Nu wordt per tekst opgezocht en gaan alleen de ontbrekende teksten naar DeepL. Zie ZOHO#11191 https://support.yard.nl/support/yarddigital/ShowHomePage.do#Cases/dv/300585000070220196

Bezoekers zonder edit_posts schrijven niets naar de cache. Staat een deel van hun teksten in de cache, dan krijgen ze alleen dat deel terug, zonder DeepL-call en zonder dat het meetelt voor de rate limit. Ontbrekende teksten worden dus pas vertaald zodra een editor de pagina bezoekt. Zodat editors zien welke pagina's dat nodig hebben, krijgt een gecachete taal in de admin-kolom een gele badge met het aantal bezoekers dat een onvolledig antwoord kreeg.

De rate limit voor bezoekers gaat van 3 naar 9 per minuut, omdat een request nu meetelt zodra geen van de gevraagde teksten gecachet is. Editors tellen niet meer mee.

Live op geertruidenberg.nl: TR is geel omdat een bezoeker daar een onvolledig antwoord kreeg.

Vertaalcache-kolom met gele TR-badge

The cache stored a complete response per object and language and returned
it regardless of the requested texts. Texts outside the post content (menu,
footer, news list, greetings) that changed after caching were never
translated, and texts cached by logged-in users (admin bar) leaked into
visitor responses.

Cached translations are now looked up per text. Missing texts are sent to
DeepL, merged into the cache when the user may cache, and the response
contains exactly the requested texts. Requests with missing texts count
towards the rate limit; when rate limited, the cached texts are returned
instead of a 429.
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Composer package changes
Dev Packages Operation Base Target Link License
filp/whoops New - 2.18.6 Compare MIT
myclabs/deep-copy Upgraded 1.12.1 1.14.0 Compare MIT
nikic/php-parser Upgraded v5.3.1 v5.9.0 Compare BSD-3-Clause
nunomaduro/collision New - v6.4.0 Compare MIT
pestphp/pest New - v1.23.1 Compare MIT
pestphp/pest-plugin New - v1.1.0 Compare MIT
phpunit/phpunit Upgraded 9.6.21 9.6.38 Compare BSD-3-Clause
sebastian/comparator Upgraded 4.0.8 4.0.10 Compare BSD-3-Clause
sebastian/exporter Upgraded 4.0.6 4.0.9 Compare BSD-3-Clause
sebastian/global-state Upgraded 5.0.7 5.0.8 Compare BSD-3-Clause
sebastian/recursion-context Upgraded 4.0.5 4.0.7 Compare BSD-3-Clause
symfony/cache-contracts Upgraded v3.5.0 v3.7.1 Compare MIT
symfony/console Upgraded v6.4.15 v6.4.47 Compare MIT
symfony/deprecation-contracts Upgraded v3.5.0 v3.7.1 Compare MIT
symfony/event-dispatcher-contracts Upgraded v3.5.0 v3.7.1 Compare MIT
symfony/polyfill-ctype Upgraded v1.31.0 v1.37.0 Compare MIT
symfony/polyfill-intl-grapheme Upgraded v1.31.0 v1.43.0 Compare MIT
symfony/polyfill-intl-normalizer Upgraded v1.31.0 v1.43.0 Compare MIT
symfony/polyfill-mbstring Upgraded v1.31.0 v1.43.0 Compare MIT
symfony/polyfill-php73 Upgraded v1.31.0 v1.37.0 Compare MIT
symfony/polyfill-php80 Upgraded v1.31.0 v1.43.0 Compare MIT
symfony/service-contracts Upgraded v3.5.0 v3.7.3 Compare MIT
symfony/string Upgraded v6.4.15 v6.4.46 Compare MIT
theseer/tokenizer Upgraded 1.2.3 1.3.1 Compare BSD-3-Clause
composer-plugin-api (effective) Changed >=1.0 <3.0 >=1.1 <3.0
ext-iconv (effective) New - *
ext-xml (effective) Removed * -

@SimonvanWijhe
SimonvanWijhe marked this pull request as ready for review October 7, 2026 13:20
@SimonvanWijhe
SimonvanWijhe requested a review from a team as a code owner October 7, 2026 13:20
@SimonvanWijhe
SimonvanWijhe requested review from Rovasch, ShunLuk, dtakken, ictbeheer, mvdhoek1 and rivanuff and a balanced review from Copilot October 7, 2026 13:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Exempt requests still consume the shared rate limit, and concurrent cache writes can lose translations.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)
What changed in this PR

Updates translation caching to operate per text, requesting only cache misses from DeepL and returning cached results when rate-limited.

Changes:

  • Adds per-text cache lookup and merging.
  • Revises rate-limit behavior for cached requests.
  • Adds Pest unit tests and documentation.
File Description
src/​Services/​TranslationService.php Implements per-text caching and merging.
src/​Controllers/​RestAPIController.php Handles cache-aware rate limiting.
tests/​Unit/​TranslationServiceTest.php Tests translation cache behavior.
tests/​Unit/​RestAPIControllerTest.php Tests rate-limit behavior.
tests/​Pest.php Provides WordPress test doubles.
phpunit.xml Configures the unit-test suite.
composer.json Adds Pest and a test command.
composer.lock Locks testing dependencies.
README.md Documents the updated behavior.
readme.txt Updates WordPress plugin documentation.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Controllers/RestAPIController.php Outdated
// Apply rate limit check if object ID is absent or translation is not cached when an object ID is present.
if ( ! $cached_translation ) {
if ( $this->is_rate_limit_exceeded() && ! $user_has_cache_capability ) {
if ( array() !== $missing_texts && $this->is_rate_limit_exceeded() && ! $user_has_cache_capability ) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aangepast, de capability-check staat nu vooraan, dus editors hogen de teller niet meer op. Er is een test bij die dat afdekt.

Comment on lines +65 to +66
if ( $cache ) {
$this->repository->store_translation( $object_id, $target_lang, $merged );

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bewust zo gelaten. Alleen gebruikers met de cache-capability schrijven naar de cache, dus gelijktijdige writes zijn zeldzaam. Gaat het toch mis, dan wordt een tekst bij het volgende editorbezoek opnieuw vertaald en opgeslagen. Een lock of losse meta per tekst is die complexiteit niet waard.

SimonvanWijhe and others added 2 commits October 7, 2026 17:07
Visitors without the cache capability never write to the cache, so any
text missing from an editor's snapshot made every language switch count
towards the rate limit and ended in a 429 after three switches. When the
cache contains any of the requested texts, visitors now get those cached
texts only, without a DeepL call or rate limit hit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@mvdhoek1 mvdhoek1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verhoging van de rate limit lijkt mij ook handiger!

Comment thread src/Services/TranslationService.php Outdated

return $translation;
/**
* @since 2.2.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Waarschijnlijk gaat niemand conflicteren met andere versies maar in feature branches zou ik @since NEXT gebruiken. (Alleen niet vergeten om deze aan te passen na de release)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aangepast naar @since NEXT.

Comment thread src/Services/TranslationService.php Outdated
*/
public function missing_texts( array $text, array $cached_translation ): array
{
return array_values( array_unique( array_diff( $text, array_keys( $this->translations_by_text( $cached_translation ) ) ) ) );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ik zou hem iets duidelijker schrijven:

public function missing_texts( array $text, array $cached_translation ): array
{
    $translations_by_text = $this->translations_by_text( $cached_translation );
    $cached_texts = array_keys( $translations_by_text );
    $missing_texts = array_diff( $text, $cached_texts );
    $unique_missing_texts = array_unique( $missing_texts );

    return array_values( $unique_missing_texts );
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overgenomen.

Comment thread src/Controllers/RestAPIController.php Outdated
}
}

if ( array() !== $missing_texts && $this->is_rate_limit_exceeded() && ! $user_has_cache_capability ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Foei! Ik zie dat ik had moeten beginnen met ! $user_has_cache_capability. Nu hogen we de rate limit ook op voor editors.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aangepast, de capability-check staat nu vooraan, dus editors hogen de teller niet meer op. Er is een test bij die dat afdekt.

Comment thread src/Controllers/RestAPIController.php Outdated
$cached_texts = $this->service->translations_for( $text, $cached_translation ?? array() );

if ( array() !== $cached_texts ) {
return new WP_REST_Response( $cached_texts );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hierdoor zien editors in de admin columns niet meer hoe volledig een cache is. Een deels gevulde cache staat gewoon als "gecachet" en bezoekers die alleen de gecachete subset krijgen verhogen de teller niet.

Omdat ontbrekende teksten pas vertaald worden bij een editorbezoek, moeten editors kunnen zien welke pagina's (en talen) dat nodig hebben.

Wellicht een aparte teller hiervoor toevoegen?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Goed punt. Er is nu een aparte teller per taal voor bezoekers die maar een deel van hun teksten uit de cache kregen. In de kolom staat die taal dan geel met het aantal, in plaats van groen. Een editorbezoek dat de ontbrekende teksten opslaat of het wissen van de cache zet de teller terug.

Comment thread README.md Outdated
- **Serving Cached Translations:** If a cached translation is newer than the `post_modified` date of the object, the cached version is served.
- **Serving Cached Translations:** Translations are cached per text. If a cached translation is newer than the `post_modified` date of the object, every requested text found in the cache is served from it. Texts that are not cached yet (for example a changed menu or footer) are fetched from DeepL and merged into the cache. The response contains exactly the requested texts.
- **Fetching New Translations:** When the `post_modified` date of the object is more recent than the cached translation, a new translation is fetched from DeepL. Once retrieved, this translation is immediately cached for future use.
- **Cache Authorization:** Only logged-in users with the `edit_posts` capability (or a custom capability configured via the `yard::deepl/cache_capability` filter) are permitted to create new cache entries. Requests from users without this capability will still return a translation from DeepL, but the result will not be stored in the cache.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deze regel klopt niet meer met deze PR. Graag dezelfde aanpassing ook in readme.txt.

Suggested change
- **Cache Authorization:** Only logged-in users with the `edit_posts` capability (or a custom capability configured via the `yard::deepl/cache_capability` filter) are permitted to create new cache entries. Requests from users without this capability will still return a translation from DeepL, but the result will not be stored in the cache.
- **Cache Authorization:** Only logged-in users with the `edit_posts` capability (or a custom capability configured via the `yard::deepl/cache_capability` filter) are permitted to create new cache entries. Requests from users without this
capability never write to the cache: when any of the requested texts are cached, only those cached texts are returned; when none are cached, the texts are translated by DeepL without storing the result.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overgenomen in README.md en readme.txt, met daarbij een uitleg van de gele badge.

…e limit

Requests from users with the cache capability incremented the per-IP
rate limit counter before the capability was checked, so editors used
up the budget of visitors behind the same IP.

Visitors served only part of their texts from the cache left no trace,
so a partially filled cache showed as fully cached in the admin column.
Those requests are now counted per language and shown as a yellow
badge until an editor visit stores the missing texts or the cache is
cleared.

Also writes out missing_texts() step by step and marks new methods
with @SInCE NEXT, as requested in review.
@SimonvanWijhe
SimonvanWijhe marked this pull request as draft November 4, 2026 15:15
Clearing the cache via the metabox left _ydpl_uncached_request_count_<lang> behind, so the grey badge kept showing an old count for a post without cache.
@SimonvanWijhe
SimonvanWijhe marked this pull request as ready for review November 4, 2026 15:40
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… post types

Visitors without the cache capability now receive texts missing from the
cache from DeepL, merged with the cached texts but not stored. When the
rate limit is reached they fall back to the cached texts only.

The cache metabox and translation cache column now default to every public
post type except attachments, so editors also see the cache status on
custom post types such as pdc-item. Columns are registered on admin_init so
post types registered later on init are included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@SimonvanWijhe
SimonvanWijhe marked this pull request as draft November 5, 2026 16:19
@SimonvanWijhe
SimonvanWijhe marked this pull request as ready for review November 5, 2026 16:29
@SimonvanWijhe
SimonvanWijhe merged commit 62aa7ae into main Nov 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants