Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b9195e7
(refactor): implement security measures for usage of the REST API end…
Feb 25, 2025
1c84c16
refactor: enqueue admin assets only on editor page
mvdhoek1 May 21, 2026
38898af
refactor: post meta 'ydpl_clear_deepl_translation_cache' always set t…
mvdhoek1 May 21, 2026
07b81ae
refactor: only cache translation when logged in user can edit posts
mvdhoek1 May 21, 2026
2d7cb17
docs: add @since NEXT annotations to new methods
mvdhoek1 May 21, 2026
28390ec
fix: remove dist from gitignore
mvdhoek1 May 21, 2026
effc26a
refactor: exclude editors from rate limiting
mvdhoek1 May 21, 2026
4c5b561
chore: update translation files
mvdhoek1 May 21, 2026
a8ef79d
refactor: more specific validations and same origin check
mvdhoek1 May 26, 2026
b626720
refactor: rename object_has_cached_translation to get_cached_translat…
mvdhoek1 May 26, 2026
64d4607
deprecate: rename filter yard::deepl/disable_cache_metabox_post_types…
mvdhoek1 May 26, 2026
4cd48af
refactor: validate nonce with fallback to legacy nonce
mvdhoek1 May 26, 2026
5b3549e
refactor: rename object_has_cached_translation to get_cached_translat…
mvdhoek1 May 26, 2026
b07c9db
refactor: more specific validations
mvdhoek1 May 26, 2026
d4bc109
chore: update README files
mvdhoek1 May 26, 2026
68c4177
refactor: duplicated cached translation lookup
mvdhoek1 May 26, 2026
83907af
refactor: include port in same origin validations
mvdhoek1 May 26, 2026
01dead2
chore: update translation files
mvdhoek1 May 26, 2026
bbe4238
refactor: move controller validation logic to register_rest_route val…
mvdhoek1 May 26, 2026
44554df
refactor: verify existence of connected post for valid object IDs
mvdhoek1 May 27, 2026
cb53eee
refactor: delete cached translations
mvdhoek1 May 27, 2026
3d5c5e2
refactor: remove redundant esc_attr() wrapper around checked()
mvdhoek1 May 27, 2026
78c551b
chore: update translation files
mvdhoek1 May 27, 2026
8bf0fea
feat: add cached translations admin columns to configured post types
mvdhoek1 May 27, 2026
7de9193
feat: add translation cache status column with per-language uncached …
mvdhoek1 May 28, 2026
12e4415
refactor: verify type of value variable in sanitize_callback
mvdhoek1 May 28, 2026
348e919
refactor: verify if the current user can edit this post and custom me…
mvdhoek1 May 28, 2026
30398bc
fix: recalculation of strtotime(post_modified) on every iteration
mvdhoek1 May 28, 2026
43916f4
refactor: extract is_cache_disabled() and apply disable-flag to all c…
mvdhoek1 May 28, 2026
f4ebbce
chore: update readme files
mvdhoek1 May 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
.phpunit.result.cache

# Folders
dist
node_modules
tests/coverage
vendor
Expand Down
127 changes: 95 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Requires at least: 6.0
Requires PHP: 8.0
Stable tag: 1.1.0
Tags: deepl, translating, secure
Tested up to: 6.7.1
Tested up to: 6.8.5

This plugin registers secure API endpoints that allow you to request translations directly from DeepL without exposing your DeepL API-key.

Expand All @@ -22,21 +22,39 @@ When providing translations to website visitors, you can configure which languag

Each object that is translated will store its cached translation in the `wp_postmeta` table within the database. This caching mechanism ensures that translations are efficiently reused, reducing unnecessary API requests to DeepL and saving costs.

- **Serving Cached Translations:** If a cached translation is newer than the `post_modified` date of the object, the cached version is served.
- **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.
- **Serving Cached Translations:** If a cached translation is newer than the `post_modified` date of the object, the cached version is served.
- **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.

This approach minimizes the number of API calls to DeepL, ensuring translations are kept up to date only when necessary.

### Admin: Cache Metabox

A metabox labeled **Yard DeepL** is displayed on the edit screen of supported post types (default: `page`). It provides two options:

- **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.
- **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. 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.

- **Service:** DeepL API (<https://www.deepl.com>)
- **Purpose:** To translate text from one language to another based on the provided target language.
- **Data Sent:** Text content for translation, the target language code, and the DeepL API key (handled securely and never exposed to users).
- **Conditions:** Data is sent when a request for translation is initiated.
- **Privacy Policy:** [DeepL Privacy Policy](https://www.deepl.com/privacy)
- **Terms of Service:** [DeepL Terms of Service](https://www.deepl.com/pro-license)
- **Service:** DeepL API (<https://www.deepl.com>)
- **Purpose:** To translate text from one language to another based on the provided target language.
- **Data Sent:** Text content for translation, the target language code, and the DeepL API key (handled securely and never exposed to users).
- **Conditions:** Data is sent when a request for translation is initiated.
- **Privacy Policy:** [DeepL Privacy Policy](https://www.deepl.com/privacy)
- **Terms of Service:** [DeepL Terms of Service](https://www.deepl.com/pro-license)

## Installation

Expand All @@ -62,54 +80,99 @@ If you need to work with development dependencies, follow these steps:

The API endpoints registered by this plugin are secured using a WordPress nonce. The nonce is passed to the front-end using the `wp_localize_script` function and is stored in a global JavaScript object `ydpl` which contains the following properties:

- `ydpl_translate_post_id`: The ID of the post to be translated.
- `ydpl_rest_translate_url`: The URL of the API endpoint for translation requests.
- `ydpl_supported_languages`: The list of languages supported for translation.
- `ydpl_api_request_nonce`: The nonce used for API validation.
- `ydpl_translate_post_id`: The ID of the post to be translated.
- `ydpl_rest_translate_url`: The URL of the API endpoint for translation requests.
- `ydpl_supported_languages`: The list of languages supported for translation.
- `ydpl_api_request_nonce`: The nonce used for API validation.

When making requests to the API, ensure that the nonce is included in the request headers. The header should be named `X-WP-Nonce`, and it should contain a nonce created with the `wp_rest` action (available via `ydpl.ydpl_api_request_nonce` on the front-end).

#### Rate Limiting

When making requests to the API, ensure that the nonce is included in the request headers. The header should be named `nonce`, and it should contain the value of `ydpl_api_request_nonce`.
To prevent abuse, unauthenticated requests (or requests from users without the cache capability) are limited to **3 requests per 60 seconds** per IP address. Authenticated users with the required cache capability (default: `edit_posts`) are exempt from this rate limit.

#### Cache Capability

Only users with the `edit_posts` capability can trigger cache creation. This can be customized using the `yard::deepl/cache_capability` filter (see [Filters](#filters)).

#### Example

##### Request

```javascript
var xhr = new XMLHttpRequest();
xhr.open('POST', ydpl.ydpl_rest_translate_url, true);
xhr.open( 'POST', ydpl.ydpl_rest_translate_url, true );

// Set request headers
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('nonce', ydpl.ydpl_api_request_nonce);
xhr.setRequestHeader( 'Content-Type', 'application/json' );
xhr.setRequestHeader( 'X-WP-Nonce', ydpl.ydpl_api_request_nonce );

// Handle response
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log('Translation:', JSON.parse(xhr.responseText));
} else if (xhr.readyState === 4) {
console.error('Error:', xhr.statusText);
}
if ( xhr.readyState === 4 && xhr.status === 200 ) {
console.log( 'Translation:', JSON.parse( xhr.responseText ) );
} else if ( xhr.readyState === 4 ) {
console.error( 'Error:', xhr.statusText );
}
};

// Prepare and send the request body
var data = JSON.stringify({
text: ["Look another test"],
target_lang: "DE"
});
var data = JSON.stringify( {
text: [ 'Look another test' ],
target_lang: 'DE',
} );

xhr.send(data);
xhr.send( data );
```

##### Response

```javascript
[
{
"text": "Look another test!",
"translation": "Sehen Sie sich einen weiteren Test an!"
}
]
{
text: 'Look another test!',
translation: 'Sehen Sie sich einen weiteren Test an!',
},
];
```

## Filters

| Filter | Default | Description |
| -------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `yard::deepl/cache_capability` | `'edit_posts'` | The WordPress capability required to create cache entries. Users without this capability receive translations but results are not cached. |
| `yard::deepl/cache_metabox_post_types` | `['page']` | Post types on which the Yard DeepL cache metabox is displayed. |
| ~~`yard::deepl/disable_cache_metabox_post_types`~~ | — | **Deprecated.** Use `yard::deepl/cache_metabox_post_types` instead. |

## Changelog

### NEXT (unreleased)

- Add: same-origin check for REST API requests
- Add: rate limiting for unauthenticated / low-privilege requests (3 req / 60 s per IP)
- Add: cache creation restricted to users with `edit_posts` capability (configurable via filter)
- Add: `yard::deepl/cache_capability` filter
- Add: Translation cache column on post list screens showing cached languages and per-language uncached call counts, to help editors prioritise which posts to cache
- Change: deprecated `yard::deepl/disable_cache_metabox_post_types` in favour of `yard::deepl/cache_metabox_post_types`
- Change: nonce validation now also accepts the standard `wp_rest` nonce action as a fallback

### 1.1.0 (Jan 31, 2025)

- Add: disable DeepL translation cache metabox
- Change: use init hook in plugin bootstrap construct, fixes translations for WordPress 6.7

### 1.0.2 (Jan 08, 2025)

- Change: update all occurrences of 'deepl' to 'DeepL' for consistency

### 1.0.1 (Jan 07, 2025)

- Change: processed corrections

### 1.0.0 (Oct 18, 2024)

- Init: first release!

## About us

[![banner](https://raw.githubusercontent.com/yardinternet/.github/refs/heads/main/profile/assets/small-banner-github.svg)](https://www.yard.nl/werken-bij/)
37 changes: 37 additions & 0 deletions assets/css/admin-columns.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.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--uncached {
background: #f0f0f1;
color: #3c434a;
}

.ydpl-badge-count {
font-weight: 400;
font-size: 9px;
opacity: 0.7;
}

.ydpl-cache-status--none {
color: #8c8f94;
}
7 changes: 7 additions & 0 deletions assets/css/editor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.ydpl-metabox-row:not( :first-child ) {
margin-top: 1rem;
}

.ydpl-metabox-row > p {
font-weight: 500;
}
1 change: 1 addition & 0 deletions dist/admin-columns.asset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?php return array('dependencies' => array(), 'version' => '1d877b8f32d5c2ebba6e');
1 change: 1 addition & 0 deletions dist/admin-columns.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file added dist/admin-columns.js
Empty file.
1 change: 1 addition & 0 deletions dist/editor.asset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?php return array('dependencies' => array(), 'version' => '045a517d31b6755cceb1');
1 change: 1 addition & 0 deletions dist/editor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.ydpl-metabox-row:not(:first-child){margin-top:1rem}.ydpl-metabox-row>p{font-weight:500}
Empty file added dist/editor.js
Empty file.
Binary file modified languages/yard-deepl-nl_NL.mo
Binary file not shown.
118 changes: 76 additions & 42 deletions languages/yard-deepl-nl_NL.po
Original file line number Diff line number Diff line change
@@ -1,71 +1,39 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/yard-deepl\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-09-06T17:21:36+00:00\n"
"PO-Revision-Date: 2024-09-06T17:21:36+00:00\n"
"POT-Creation-Date: 2026-05-21T12:17:13+00:00\n"
"PO-Revision-Date: 2026-05-27T14:36:10+00:00\n"
"Language: \n"

#. Plugin Name of the plugin
#: yard-deepl.php:8
#: src/Providers/MetaBoxServiceProvider.php:35
#: src/Providers/SettingsServiceProvider.php:44
#: src/Providers/SettingsServiceProvider.php:45
#: src/Providers/MetaboxServiceProvider.php:34
msgid "Yard DeepL"
msgstr "Yard DeepL"

#. Description of the plugin
#: yard-deepl.php:9
msgid "This plugin registers secure API endpoints that allow you to request translations directly from DeepL without exposing your Deepl API-key"
msgstr "Deze plugin registreert beveiligde API-eindpunten waarmee je vertalingen direct bij DeepL kunt opvragen zonder je DeepL API-sleutel bloot te stellen aan de buitenwereld"

#. Author of the plugin
msgid "Yard | Digital"
msgstr "Yard | Digital"
#: yard-deepl.php:11
msgid "Yard | Digital Agency"
msgstr "Yard | Digital Agency"

#. Author URI of the plugin
#: yard-deepl.php:12
msgid "https://www.yard.nl"
msgstr "https://www.yard.nl"

#: src/Providers/MetaboxServiceProvider.php:51
msgid "Disable translation cache when this object contains dynamic content."
msgstr "Schakel de vertalingscache uit wanneer dit object dynamische inhoud bevat."

#: src/Providers/MetaboxServiceProvider.php:76
msgid "Disable translation cache?"
msgstr "Vertalingscache uitschakelen?"

#: src/Providers/SettingsServiceProvider.php:66
msgid "Settings"
msgstr "Instellingen"

#: src/Providers/SettingsServiceProvider.php:73
msgid "Deepl API key"
msgstr "Deepl API-sleutel"

#: src/Providers/SettingsServiceProvider.php:82
msgid "Deepl supported languages"
msgstr "Deepl ondersteunde talen"

#: src/Providers/SettingsServiceProvider.php:98
msgid "Parameter object_id mandatory"
msgstr "Parameter object_id verplicht"

#: src/Views/admin/settings-page.php:15
msgid "Save"
msgstr "Opslaan"

#: src/Views/admin/partials/settings/settings-description-general.php:10
msgid "website"
msgstr "website"

#: src/Views/admin/partials/settings/settings-description-general.php:16
msgid "To query the Deepl API, an API key is required. You can find more information on their %s."
msgstr "Om de Deepl API te bevragen, is een API-sleutel vereist. Meer informatie vind je op hun %s."

#: config/php-di.php:25
msgid "Arabic"
msgstr "Arabisch"
Expand Down Expand Up @@ -197,3 +165,69 @@ msgstr "Chinees (vereenvoudigd)"
#: config/php-di.php:153
msgid "Chinese (Traditional)"
msgstr "Chinees (traditioneel)"

#: src/Providers/MetaBoxServiceProvider.php:72
msgid "Disable translation cache when this object contains dynamic content."
msgstr "Schakel de vertalingscache uit wanneer dit object dynamische inhoud bevat."

#: src/Providers/MetaBoxServiceProvider.php:75
msgid "Disable translation cache?"
msgstr "Vertalingscache uitschakelen?"

#: src/Providers/MetaBoxServiceProvider.php:78
msgid "Clear cached translations on save."
msgstr "Verwijder vertaalcache bij opslaan."

#: src/Providers/MetaBoxServiceProvider.php:81
msgid "Clear translation cache?"
msgstr "Vertaalcache wissen?"

#: src/Providers/SettingsServiceProvider.php:69
msgid "Settings"
msgstr "Instellingen"

#: src/Providers/SettingsServiceProvider.php:76
msgid "Deepl API key"
msgstr "Deepl API-sleutel"

#: src/Providers/SettingsServiceProvider.php:85
msgid "Deepl supported languages"
msgstr "Deepl ondersteunde talen"

#: src/Providers/SettingsServiceProvider.php:101
msgid "Parameter object_id mandatory"
msgstr "Parameter object_id verplicht"

#. Translators: %s is the URL to the Deepl API documentation page.
#: src/Views/admin/partials/settings/settings-description-general.php:10
msgid "website"
msgstr "website"

#. Translators: %s is the link to the Deepl API documentation website.
#: src/Views/admin/partials/settings/settings-description-general.php:16
#, php-format
msgid "To query the Deepl API, an API key is required. You can find more information on their %s."
msgstr "Om de Deepl API te bevragen, is een API-sleutel vereist. Meer informatie vind je op hun %s."

#: src/Providers/AdminColumnsServiceProvider.php:53
msgid "Translation cache"
msgstr "Vertaalcache"

#: src/Providers/AdminColumnsServiceProvider.php:70
msgid "No languages configured"
msgstr "Geen talen geconfigureerd"

#: src/Providers/AdminColumnsServiceProvider.php:105
msgid "No cached translations"
msgstr "Geen gecachede vertalingen"

#. Translators: %d is the number of uncached DeepL API calls made for this post and language.
#: src/Providers/AdminColumnsServiceProvider.php:94
msgid "%d uncached call"
msgid_plural "%d uncached calls"
msgstr[0] "%d aanroep zonder cache"
msgstr[1] "%d aanroepen zonder cache"

#: src/Views/admin/settings-page.php:15
msgid "Save"
msgstr "Opslaan"
Loading