Search by

cyllene-digital / ai-translation-bundle

CylleneDigital

Symfony bundle to edit translations at runtime: database overrides applied over your translation files, AI suggestions (OpenAI-compatible, Anthropic, DeepL) applied only after human review, coverage, and CSV/XLIFF import/export.

Package info

github.com/CylleneDigital/AiTranslationBundle

Type:symfony-bundle

pkg:composer/cyllene-digital/ai-translation-bundle

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v1.0.0 2026-10-06 06:10 UTC

This package is auto-updated.

Last update: 2026-10-07 16:19:16 UTC


README

AI Translation Bundle

AI Translation Bundle

License Latest version Build Security

A Symfony bundle to edit your application's translations at runtime: overrides are stored in the database and applied on top of your translations/ files, which are never modified, and AI suggestions fill the missing keys, applied only once a human approves them.

It is an engine without user interface: a console and a PHP API, on which any application can build its own screens.

Compatibility

Component Versions
PHP ^8.3
Symfony ^7.4 || ^8.0
Doctrine ORM ^3.5 (DBAL 3 or 4)
Database MySQL, MariaDB, PostgreSQL, SQLite
PHP extensions dom, libxml; intl recommended: without it, ICU overrides are served as stored and an invalid ICU pattern is not caught at save time

An API key for at least one AI provider (OpenAI, Anthropic, DeepL, or any OpenAI-compatible server such as Mistral or a local Ollama) is needed for the suggestions; the overrides work without.

What this bundle does

Installation

composer require cyllene-digital/ai-translation-bundle

The Flex recipe registers the bundle and adds a commented config/packages/cyllene_digital_ai_translation.yaml (it is a contrib recipe: answer "yes" when Flex asks, or set "allow-contrib": true under extra.symfony once and for all). Without Flex:

// config/bundles.php
CylleneDigital\AiTranslationBundle\CylleneDigitalAiTranslationBundle::class => ['all' => true],

The bundle ships its entity mapping, not migrations: generate one in your application (with doctrine/doctrine-migrations-bundle; doctrine:schema:update does without it):

bin/console doctrine:migrations:diff && bin/console doctrine:migrations:migrate

Run the diff again after each bundle update: a mapping change needs its own migration.

On DBAL 3, set doctrine.dbal.use_savepoints: true (why).

Configuration

The minimal configuration declares one AI provider:

# config/packages/cyllene_digital_ai_translation.yaml
cyllene_digital_ai_translation:
    providers:
        openai:
            type: openai            # or anthropic, deepl, default (any /chat/completions API)
            api_key: '%env(OPENAI_API_KEY)%'

Every other key (translations path, default locale, extra translation roots, AI context, cache TTLs) has a default: configuration reference. Each provider type has its own page: provider bridges.

How to start

  1. Open the console, a menu of guided tasks: browse and edit translations, generate and review suggestions, import, export, coverage.

    bin/console cyllene:ai-translation
  2. Generate suggestions for a locale, the cost estimate first, then for real:

    bin/console cyllene:ai-translation:generate --target=fr --dry-run
    bin/console cyllene:ai-translation:generate --target=fr
  3. Review them in the console ("Review the pending suggestions"): approving one writes the override, served right away; it writes nothing when the entry already shows that value, and removes the stored override when the approved value is the one the entry falls back to.

  4. Check the coverage, in CI if you like (--min=95 fails under 95 %):

    bin/console cyllene:ai-translation:coverage

Every command and its options: docs/operations/commands.md.

Synchronous vs asynchronous generation

Synchronous generation (default)

Out of the box, generation is synchronous: it runs where it is called, with nothing more to install or configure. That is how the console works: cyllene:ai-translation and generate run the generation in the command, show its progress and return a meaningful exit code.

Asynchronous generation

To run generation in a worker instead, add the Doctrine transport:

composer require symfony/doctrine-messenger

The bundle then configures the rest itself: a cyllene_ai_translation transport (a queue in your database, never retried: a retry would bill the provider twice), with GenerateSuggestionsMessage routed to it.

From your code, dispatch one message per catalogue and target locale:

use CylleneDigital\AiTranslationBundle\Message\GenerateSuggestionsMessage;
use Symfony\Component\Messenger\MessageBusInterface;

final class TranslateCatalogue
{
    public function __construct(private readonly MessageBusInterface $bus)
    {
    }

    public function __invoke(): void
    {
        // catalogue, target locale, source locale
        $this->bus->dispatch(new GenerateSuggestionsMessage('messages', 'fr', 'en'));
    }
}

Dispatch it on a bus without the doctrine_transaction middleware. Sylius' default bus has it, and a run inside a transaction is refused: a rollback would discard suggestions the provider has already billed. Never on a transactional bus shows a dedicated bus.

And run a worker:

bin/console messenger:consume cyllene_ai_translation

The console stays synchronous either way: it never dispatches the message. Another broker (RabbitMQ, Redis…), your own routing, the failure transport and multi-server locks: configuration reference.

Documentation

Page What it covers
Index The reading guide: concepts, bridges, operations, design decisions
Overrides The decorated translator, scopes, the cache layers, automatic invalidation
Catalogues The scan, additional roots, the same-language fallback chain, what "missing" means
Coverage The default locale, what counts as translated, the CI gate, the invisible drift
Suggestions Lifecycle, idempotent generation, the placeholder guard, the audit trail
Manual override Editing one translation: the four doors, save guarantees, orphans
Scopes Per-site/brand/channel values, shadowing order, the host-side provider
Configuration reference Every cyllene_digital_ai_translation key, type and default
Provider bridges default / openai / anthropic / deepl / custom: options, wire formats, error behaviour
CLI commands Every command with its options
Caches What is invalidated automatically, multi-server and worker setups
Running on several servers The shared cache pool and lock store a multi-node deployment needs
Doctrine DBAL 3 The savepoints setting a DBAL 3 host needs, and why

Extending

Every strategic decision can be replaced or observed from the host application, without touching the bundle's code. All classes below live under CylleneDigital\AiTranslationBundle\.

  • PHP API: inject Override\TranslationManagerInterface (catalogues and overrides) and Suggestion\TranslationAiServiceInterface (generation and review) to build your own screens; type-hint the interfaces, so an integration can decorate them. An integration's import and export features use Transfer\OverrideImporter, OverrideExporter, ViewExporter and OverrideImportTemplates.

  • Locale list: by default the available locales are the ones discovered in the scanned files (Catalogue\ScannedLocaleProvider). To drive the list yourself, from the host's own locale registry for instance, alias Catalogue\LocaleProviderInterface to your service.

  • Override author: each saved override records who made it. Out of the box nobody is recorded (Override\NullAuthorProvider); alias Override\AuthorProviderInterface to sign with the logged-in admin, an API token…

  • Scope: overrides can carry an optional scope that the host maps to its own concept: a site, a brand, a tenant, a sales channel… Implement Scope\ScopeProviderInterface to declare the known scopes and the active one; the scoped value then shadows the global one (concepts/scopes.md).

  • Custom AI provider: implement Provider\TranslationAiProviderInterface (the service tag is autoconfigured) and your provider becomes selectable by its name, like a built-in bridge (it can also serve as default_provider); also implement Provider\CostEstimatingProviderInterface to feed the pre-run cost estimate. Full walkthrough: provider_bridge/custom.md.

  • Events: OverrideSavedEvent, OverrideRemovedEvent, SuggestionApprovedEvent and SuggestionRejectedEvent are dispatched at each mutation, so the host can audit, notify or invalidate whatever it needs. They carry managed entities (OverrideRemovedEvent: the removed row's coordinates): read them, do not modify them (the next flush would write the change). The override events and SuggestionApprovedEvent are dispatched once the bundle's transaction has committed (inside one of yours, before your commit); SuggestionRejectedEvent comes right before the row is deleted, so before the commit. It also fires when a suggestion is superseded (by an override written for its key, or, for an errored one, by the files filling its key), and $supersededBy says which (events).

  • Errors: every exception a host is expected to handle (a refused approval, an unusable import file, an unknown provider, a provider failure…) implements Exception\ExceptionInterface, so it can catch them all in one place. Programming errors stay plain SPL exceptions: a generation run inside a transaction, an import or export format outside OverrideExporter::FORMATS (check supports() first), two providers with the same name.

  • Async generation: Message\GenerateSuggestionsMessage, see Asynchronous generation. A failed run is reported as unrecoverable, so the message reaches your failure transport instead of being retried.

Contributing

CONTRIBUTING.md: running the test suite, standards, what has to pass.

A security flaw is reported privately: SECURITY.md. Do not open it as a public issue.

Provenance and licence

Released under the MIT licence (see LICENSE). Model prices for the cost estimate come from the public LiteLLM price list, fetched at runtime.

Package: cyllene-digital/ai-translation-bundle

Maintained by Cyllene, on GitHub as @CylleneDigital