Skip to content
FriendsOfREDAXOPublic

About

vTrans for the automatic translation and rephrasing of content

Resources

Code of conduct

Stars

6 stars

Watchers

1 watching

Forks

Repository files navigation

vTrans BETA for REDAXO 5

vTrans bundles several text-processing APIs behind a single interface, stores the results in the database, and provides backend pages for testing, analysis, and maintenance.

The primary use case is translation. With LLM-based providers (the OpenAI provider or ai_platform), it can also be used for other scenarios where the source and target language are the same, for example summarizing, rephrasing, or editing content.

Note: vTrans is a release candidate for version 1.0. Production use is recommended only after appropriate testing and under close monitoring.


Installation

Install it via the REDAXO installer or copy it manually to redaxo/src/addons/vtrans and activate it in the backend.

Requirements:

  • REDAXO >= 5.17.0
  • PHP >= 8.2

Quick Start

  1. In the backend, open vTrans -> Connections and create a connection.
  2. Select a provider (for example deepl-api-free-v2, openai, or google-translate-basic-v2).
  3. Enter the API key, API URL, and any additional parameters.
  4. Optionally mark it as the default and/or enable it for the Playground.
  5. Open vTrans -> Playground and test it.
  6. Check out the template example and try it out. That's the quickest way to understand the concept.

Example for a DeepL Free connection:

  • Key: deepl_free
  • Label: DeepL Free
  • Provider: deepl-api-free-v2
  • API URL: https://api-free.deepl.com/v2/translate
  • API Key: YOUR_DEEPL_KEY

Notes:

  • Free keys belong to the free API URL https://api-free.deepl.com/v2/translate.
  • The default connection is used automatically when no connection value is passed in the request.

Features

  • Access several providers through one unified API
  • Manage connections centrally in the backend
  • Test requests manually in the Playground
  • DB cache is monitored via string hash, connection, language, and format
  • Stable keys (optional) for reusable content
  • Search, filter, review, and edit stored records under Data
  • Keep provider metadata such as usage or rate limits as raw data

Supported Providers / APIs

DeepL

  • Market leader with very good quality for common languages
  • deepl-api-free-v2
  • deepl-api-pro-v2
  • Supports context and customInstructions

Note: Unfortunately, DeepL's free plan (500k characters/month) no longer exists. For testing, or later as a low-cost alternative, I've set up a DeepL-compatible server. Get in touch if you're interested!

Amazon Translate

  • Good to very good translation quality
  • amazon-translate-v1
  • API-key-/credential-based depending on the provider implementation

Google Translate Basic v2

  • Good to very good translation quality
  • google-translate-basic-v2
  • API-key-based
  • No prompt options

Google Translate v3

  • Very good translation quality
  • Service-account / OAuth-based
  • No prompt options

LibreTranslate

  • Good quality - sufficient for most use cases
  • Open source - can also be self-hosted
  • libretranslate-v1
  • Optional apiKey
  • No prompt options

MyMemory

  • Simple, rather technical translation
  • mymemory-v2
  • Endpoint-based (default: https://api.mymemory.translated.net/get)
  • Optional apiKey and email
  • No prompt options

OpenAI-compatible LLMs

  • Flexible depending on the model
  • openai
  • Freely configurable endpoints and parameters
  • Supports context and customInstructions; system prompt with placeholders, see System prompt

AI Platform (ai_platform addon)

  • ai_platform
  • Delegates to a text profile of the ai_platform addon instead of calling an LLM endpoint directly
  • Provider, model, API key, temperature and token limits are all managed in the ai_platform profile — the vTrans connection only selects the profile
  • The prompt comes from the vTrans connection and works exactly as for openai, see System prompt. Supports context and customInstructions
  • The ai_platform profile's own system_prompt is deliberately not used: vTrans cannot see it, so changing it would not refresh cached translations. The connection prompt is part of the cache hash
  • The connection has no timeout of its own: the HTTP call is made by ai_platform
  • Requires the ai_platform addon; without it the provider is inert (empty profile select, clear error on use)

Fake Local

  • Useful during development
  • Generates simple test output to verify the functionality
  • Local only - no API - no costs

Costs

The costs of the different providers vary widely and usually consist of a monthly base fee (subscription) and costs per 1 million characters. There are also often free or included quotas. This is something everyone needs to compare for themselves. LibreTranslate can also be self-hosted on suitable hardware. For a large website, you should expect around 20–50 EUR per language (of course only a rough estimate).

Configuration

Configuration is done via the backend page Connections. There, connections are defined. Depending on the interface, the corresponding details are stored:

  • Key (identification)
  • Label (name)
  • Provider / API (provider / interface)
  • Debug flag
  • Timeout
  • Max. characters (a guideline, see below)
  • Playground flag (available in the Playground)
  • Various provider-specific parameters

Notes:

  • The default connection is used automatically when no individual Connection is defined in the request.
  • The default connection and availability in the Playground can be switched quickly in the Connections overview.

System prompt (LLM providers)

Leave the System-Prompt field empty and vTrans sends its default prompt, which the form shows greyed out:

You are a professional translation engine.
Translate from {source_lang_name} to {target_lang_name}.
Return only the translated text, without explanations, quotes, markdown fences, or extra comments.

A prompt of your own replaces it entirely. These placeholders are filled in per request:

Placeholder Example
{source_lang} DE (auto when detecting)
{target_lang} EN-GB
{source_lang_name} German
{target_lang_name} English – British

Name the target language in your prompt, preferably as {target_lang_name} — models handle names more reliably than codes. The form warns on save when neither target placeholder is present.

vTrans always appends the following, whatever the prompt says:

  1. The format rule — plain text, or for HTML: keep the markup and the <vtrans-ph> placeholders, which vTrans needs to restore excluded regions.
  2. context of the request, as Context:.
  3. customInstructions of the request, as a list.

The prompt is part of the cache hash; changing it refreshes cached translations.


Playground

Requests can be tested manually here.

Inputs

  • Connection
  • Source and target language
  • Format (text or html)
  • Text
  • Optional key
  • Additional context and customInstructions where supported

Key behavior

If a key is set, vTrans works with a stable dataset per model and target language.

  • If an entry with the same hash already exists, it is reused.
  • If the content has changed, the existing key-based record is updated.
  • Without a key, only the normal cache based on hash, connection, language, and format is used.

A keyed record is identified by key, source language, target language, connection and format. The same key can therefore exist as text and as html side by side without one overwriting the other.

Max. characters

Max. characters is a guideline, not a limit. When a request exceeds it, vTrans records that in the REDAXO system log (once per connection and request) and sends the request anyway. That keeps the cost visible without breaking a site whose articles are simply longer than the configured value.

Usage in Code

vTrans uses the namespace FriendsOfRedaxo\VTrans.

use FriendsOfRedaxo\VTrans\VTrans;           // Namespace for the VTrans class

$translated = VTrans::translate(
    '<p>Hello world</p>',                    // content to translate
    'en',                                    // source language (also 'auto' possible)
    'de',                                    // target language
    'html',                                  // format (text or html)
    'homepage_hero',                         // optional key
    [                                        // additional optional parameters
        'connection' => 'deepl_free',        // connection (otherwise the default connection is used)
        'context' => 'Marketing headline',   // additional context (if supported)
        'customInstructions' => [            // additional instructions (if supported)
            'Use formal tone.',
            'Keep HTML tags unchanged.',
        ],
    ]
);

echo $translated;

// Optional debug output
$meta = VTrans::getLastResultMeta();
$data = VTrans::getLastResultData();
dump($meta);
dump($data);

Supported request options include:

  • connection: key of a saved connection (recommended)
  • context: additional context for supported providers
  • customInstructions: array or multiline string with additional instructions
  • debug: enables provider debug data
  • cache: boolean (true by default). With false, DB cache lookup and persistence are skipped.
  • throwOnError: boolean. Overrides the default error behaviour (see below).

Error Behaviour

When a provider call fails — exhausted quota, timeout, wrong API key — the behaviour depends on the context:

  • Frontend: no exception is thrown. translate() returns the untranslated source text so the page stays readable. The error is written to the REDAXO system log (System > Log).
  • Backend and CLI: the exception propagates as before, so the Playground still displays it unchanged.

throwOnError overrides this in both directions:

// Throw in the frontend anyway
VTrans::translate($text, 'de', 'en', 'text', null, ['throwOnError' => true]);

// Fail silently in the backend and get the source text back
VTrans::translate($text, 'de', 'en', 'text', null, ['throwOnError' => false]);

getLastResultMeta() tells you whether the last call succeeded:

$meta = VTrans::getLastResultMeta();
if (!empty($meta['failed'])) {
    // $meta['errorType']  => quota | auth | timeout | other
    // $meta['httpStatus'] => e.g. 456 (DeepL) or 429, when it can be determined
    // $meta['error']      => the provider's message
}

The errorType is also stored in the record's data column and is therefore visible on the Data page. quota means the contingent or rate limit was reached — a later attempt may succeed. auth means the connection's credentials were rejected.

Notice for signed-in backend users

If a backend user is signed in in the same browser, the error message is additionally rendered in the frontend — as a small block after the content for format = 'html', as an appended […] hint for format = 'text'. Regular visitors see none of it, only the untranslated text.

Provider messages are redacted before they are stored, logged or displayed: query parameters such as key= or api_key=, Bearer and DeepL-Auth-Key values, and the AWS credential headers are replaced with ***. Guzzle embeds the full request URI in its exception messages, and Google Translate Basic v2 and MyMemory pass the API key as a query parameter — without redaction the key would end up in the data column, which is readable by every user with the vtrans[] permission.

Simple Template Example

This example simply translates the German original content and outputs it in another language when no content is available for that language — meaning the article is empty.

<?php
use FriendsOfRedaxo\VTrans\VTrans;

// Current language of the REDAXO article context
$curLang = rex_clang::getCurrent()->getCode();

// Content of the current article
$articleContent = $this->getArticle();

// If we are in a non-default language and there is no content yet,
// retrieve the German original content and let vTrans translate it.
if (rex_clang::getCurrentId() !== 1 && $articleContent === '') {
    // German original content from the base language (ID 1)
    $articleContentOrg = (new rex_article_content(rex_article::getCurrentId(), 1))->getArticle();

    $articleContent = VTrans::translate(
        $articleContentOrg,                         // Content to translate
        'de',                                       // Source language
        $curLang,                                   // Target language
        'html',                                     // Format
        'artCont-' . rex_article::getCurrentId()    // Key for better caching
    );
}
?>
<!DOCTYPE html>
<html lang="<?php echo htmlspecialchars($curLang, ENT_QUOTES, 'UTF-8'); ?>">
<head>
    <meta charset="utf-8">
    <title>vTrans Demo</title>
</head>
<body>
    <h1>vTrans Demo</h1>
    <!-- Simple language switcher for the demo -->
    <nav>
        <?php foreach (rex_clang::getAll(true) as $lang): ?>
            <?php if ($lang->getValue('id') === rex_clang::getCurrentId()): ?>
                | <strong><?php echo htmlspecialchars($lang->getValue('name')); ?></strong>
            <?php else: ?>
                | <a href="<?php echo rex_getUrl($this->getValue('article_id'), $lang->getValue('id')); ?>">
                    <?php echo htmlspecialchars($lang->getValue('name')); ?>
                </a>
            <?php endif; ?>
        <?php endforeach; ?>
    </nav>

    <?php echo $articleContent; ?>
</body>
</html>

In this example, only the actual content is translated. Other parts such as navigation, footer, meta data, etc. can be translated in the same way. For navigation, manual translation is often recommended, because this often also affects the URL.

No-Cache Mode

With cache => false, a request can be executed directly via the API without first checking the database and without saving the result afterwards.

$translated = VTrans::translate(
    $text,
    'de',
    'en',
    'text',
    null,
    [
        'connection' => 'openai_default',
        'cache' => false,
    ]
);

$meta = VTrans::getLastResultMeta();
$data = VTrans::getLastResultData();

In no-cache mode:

  • no database lookup is performed before the provider call
  • no result is stored in rex_vtrans
  • raw provider data is still available directly via VTrans::getLastResultData()

VTrans::getLastResultData() returns the raw data from the last request, such as usage data, rate limits, debug information, or provider-specific metadata.


HTML Filter (Exclude Content from Processing)

When using the html format, a provider-independent HTML filter automatically protects certain content before the API call and restores it after translation.

Do not translate (translate="no", .notranslate)

<span translate="no">Thomas König</span>
<span class="notranslate">REDAXO CMS</span>

Exclude entire blocks (data-vtrans-exclude)

<div data-vtrans-exclude>
    <script>var config = { lang: 'de' };</script>
    <p>This block is not sent to the API.</p>
</div>

Template syntax (Twig, Mustache, …)

{{ … }}, {% … %} and {# … #} are always protected — in text as well as inside attribute values. This matters when a template source is translated before it is rendered: otherwise a provider may "translate" {% endfor %} and break the template. Attribute values made of template syntax only (alt="{{ product.name }}") are not sent. HTML without these delimiters is not affected.

Translating attribute values (alt, title, …)

Most providers translate only text in HTML mode and leave attributes alone. vTrans therefore takes the values of translatable attributes out of their tags, sends them along in the same request as text of their own, and writes the translations back afterwards. There are no additional API calls.

Translated by default: alt, title, placeholder, aria-label, aria-description, aria-roledescription, aria-placeholder, and value on buttons (<input type="button">, submit, reset — on every other element value is data and stays untouched).

Values without letters (aria-label="2"), URLs, paths, file names (IMG_1234.jpg) and values containing markup are left alone, as is everything inside translate="no", .notranslate and data-vtrans-exclude — including a marker on the element itself, e.g. <img class="notranslate" alt="Brand name">.

This is always on and needs no configuration.

Attribute values are translated without their surrounding sentence. Short values like a brand name can come back translated literally; mark the element with translate="no" in that case.

Sanitisation of the result

Everything that is stored in the translation column is sanitised on the way in, because neither source is admin-trusted: the provider's answer, and the edit form on the Data page, which is open to every user holding the vtrans[] permission.

Removed are <script>, <style>, <iframe>, <object>, <form>, <base>, <link>, <meta>, all on* event attributes and javascript and data URLs. Ordinary article markup is kept: links, images, srcset, class, id, inline style, title, lang, dir, role, all data-* and aria-* attributes, tables and lists.

data-* and aria-* stay because Bootstrap and similar components depend on them (data-bs-toggle, data-bs-target, aria-expanded, …) and the browser never executes their values. JavaScript on your site can, though: libraries such as Knockout (data-bind) or htmx (data-hx-*) turn data attributes into code or requests. If you use such a library, keep that markup out of translated regions with .notranslate or data-vtrans-exclude.

Nothing you marked yourself is affected: your own <script> and <style> blocks and every region carrying data-vtrans-exclude, translate="no" or .notranslate are removed by the HTML filter before the API call and restored afterwards — that is, after sanitisation — so they never pass through it. A <button class="notranslate" onclick="…"> therefore keeps its handler. Sanitisation happens on write, never on read: a cached record is returned unchanged.

Records written before this version were stored unsanitised. If a translation was edited manually back then, it is worth reviewing it.

Switching it off per connection

  • HTML sanitisation on the connection page — switching it off stores the provider's answer and every manual edit on the Data page raw, and the frontend renders it as unfiltered HTML on every cache hit. Only defensible if you trust both the provider and everyone holding vtrans[]. Existing connections keep sanitisation on; the setting has to be switched off deliberately.

Prefer marking the markup in question as excluded over switching sanitisation off — an excluded region never reaches the sanitiser in the first place, and nothing else on the page is weakened.

Automatically excluded tags

  • <script>…</script>
  • <style>…</style>
  • <code>…</code>
  • <svg>…</svg>

Cache and existing records

Records whose source contains translatable attributes or data-* / aria-* attributes get a new cache hash with this version. They are re-translated once, on their next request — keyed records are updated in place — and then carry translated attributes and keep their data and ARIA attributes. Records without such attributes keep their hash and are not sent again.


Support

Credits


License

MIT, see LICENSE.

About

vTrans for the automatic translation and rephrasing of content

Resources

Code of conduct

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages