mtubis / html-translator
Translate HTML with LLMs (OpenAI, Anthropic, Gemini, Ollama, …) while preserving markup, semantics and document validity.
Requires
- php: ^8.4
- ext-dom: *
- ext-mbstring: *
- psr/cache: ^3.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.1
- psr/http-message: ^1.1 || ^2.0
- psr/log: ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95
- infection/infection: ^0.35.6
- nyholm/psr7: ^1.8.2
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.3
- phpstan/phpstan-phpunit: ^2.1
- phpstan/phpstan-strict-rules: ^2.1
- phpunit/phpunit: ^13.4
- rector/rector: ^2.7
- symfony/http-client: ^8.1
Suggests
- phpunit/phpunit: Required only when running the optional chat model adapter contract tests
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.4.0
- dev-docs/laravel-package-link
- dev-docs/core-1.0-issue-drafts
- dev-docs/62-usage-guide-and-release
- dev-feat/55-anthropic-betas
- dev-feat/54-openai-json-schema
- dev-feat/47-term-check-issues
- dev-feat/48-per-batch-do-not-translate
- dev-feat/59-unit-cache
- dev-feat/60-token-usage-stats
- dev-feat/58-token-budget-batcher
- dev-refactor/53-shared-http-secrets
- dev-fix/52-psr18-cause-without-request
- dev-docs/core-0.4-issue-drafts
- dev-feat/43-truncate-untrusted-text
- dev-feat/42-table-part-fragments
- dev-feat/41-anthropic-chat-model
- dev-feat/40-context-hints
- dev-feat/39-section-headings
- dev-feat/38-profile-in-prompt
- dev-feat/37-translation-profile
- dev-docs/core-0.3-issue-drafts
- dev-feat/30-integration-tests-and-eval
- dev-feat/29-openai-compatible-chat-model
- dev-feat/28-llm-translation-provider
- dev-feat/27-prompt-builder-response-parser
- dev-feat/26-chat-model-port
- dev-docs/core-0.2-issue-drafts
- dev-feat/20-provider-failure-details
- dev-feat/21-missing-unit-hint
- dev-test/9-golden-fixtures
- dev-feat/8-orchestration
- dev-feat/7-reassembler
- dev-feat/6-structure-validator
- dev-fix/15-do-not-translate-form-data
- dev-feat/5-segmenter-inline-encoder
- dev-feat/4-skip-rules-and-translatable-attributes
- dev-feat/3-html-loader
- dev-feat/2-domain-value-objects
- dev-chore/1-bootstrap-tooling
This package is auto-updated.
Last update: 2026-10-13 22:50:45 UTC
README
Translate HTML documents with large language models — without breaking the markup.
- Structure-safe: inline markup is sent as numbered pseudo-tags, validated after every model response and rebuilt from the original elements. Attributes, URLs and non-translatable regions (
<code>,<script>,translate="no", …) never reach the model. - Steerable: translation profiles for domain, formality, glossary and term policy — e.g. keep English technical terms in a developer-docs translation.
- Any model: OpenAI, Anthropic, Gemini, or self-hosted (Ollama, vLLM, LM Studio) behind one interface, built on PSR-18 — bring your own HTTP client.
- Honest results: partial failures are reported as issues on the result object instead of silently producing broken HTML.
Status: early development (0.x). The public API may change until 1.0. See ROADMAP.
Requirements
PHP 8.4+ (uses the native HTML5 parser Dom\HTMLDocument). For real models: any PSR-18 HTTP client with PSR-17
factories, e.g. symfony/http-client with nyholm/psr7.
Installation
composer require mtubis/html-translator:^0.4
composer require symfony/http-client nyholm/psr7 # or any other PSR-18 client
Quick start (offline)
PseudoLocalizationProvider needs no model and no network, so you can see what the library does to your HTML:
use HtmlTranslator\Domain\Locale; use HtmlTranslator\HtmlTranslator; use HtmlTranslator\Provider\Testing\PseudoLocalizationProvider; $translator = new HtmlTranslator(new PseudoLocalizationProvider()); $result = $translator->translate( '<p>Click <a href="/docs">here</a> to <strong>continue</strong>.</p>', Locale::fromString('pl'), ); echo $result->html();
<p>[!! Çļíçķ <a href="/docs">ĥéŕé</a> ţö <strong>çöñţíñüé</strong>. !!]</p>
Markup, non-translatable attributes (href, classes, …) and non-translatable content (<code>, translate="no",
scripts) are never sent to the provider and come back untouched; translatable attributes (alt, title,
placeholder, …) are translated, and <html lang> is set to the target for full documents.
Translating with a real model
Language models are used through LlmTranslationProvider and a chat model adapter. With OpenAI:
use HtmlTranslator\HtmlTranslator; use HtmlTranslator\Provider\Llm\Bridge\OpenAiCompatible\OpenAiCompatibleChatModel; use HtmlTranslator\Provider\Llm\Bridge\OpenAiCompatible\OpenAiCompatibleConfig; use HtmlTranslator\Provider\Llm\LlmTranslationProvider; use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpClient\Psr18Client; $http = new Psr18Client(HttpClient::create(['timeout' => 120])); $model = new OpenAiCompatibleChatModel($http, $http, $http, OpenAiCompatibleConfig::openAi($_ENV['OPENAI_API_KEY'], 'gpt-4o-mini')); $translator = new HtmlTranslator(new LlmTranslationProvider($model));
With Anthropic's Claude:
use HtmlTranslator\Provider\Llm\Bridge\Anthropic\AnthropicChatModel; use HtmlTranslator\Provider\Llm\Bridge\Anthropic\AnthropicConfig; $model = new AnthropicChatModel($http, $http, $http, new AnthropicConfig('claude-opus-5-5', $_ENV['ANTHROPIC_API_KEY'])); $translator = new HtmlTranslator(new LlmTranslationProvider($model));
Then translate, here with a profile for technical documentation and a PSR-6 cache, so units that were already translated are not paid for again:
use HtmlTranslator\Domain\Locale; use HtmlTranslator\Profile\Glossary; use HtmlTranslator\Profile\TranslationProfile; use HtmlTranslator\TranslationOptions; $translator = new HtmlTranslator(new LlmTranslationProvider($model), cache: $cache, logger: $logger); $result = $translator->translate($html, Locale::fromString('pl'), new TranslationOptions( source: Locale::fromString('en'), // null = let the model detect it profile: TranslationProfile::technical() ->withGlossary(Glossary::fromArray(['branch' => 'gałąź'])) ->withDoNotTranslate('GitHub'), ));
Partial failures do not throw. A unit whose translation keeps breaking the markup after corrective retries, or that the model refuses or leaves out, is handled by the fallback strategy (by default it keeps its original text) and reported, so always check the result:
$translated = $result->html(); // always valid HTML if (!$result->isComplete()) { foreach ($result->issues() as $issue) { $logger->warning($issue->message, ['unit' => $issue->unitId->toString(), 'type' => $issue->type->name]); } } $stats = $result->stats(); printf("units: %d, batches: %d, from the cache: %d\n", $stats->units, $stats->batches, $stats->cacheHits);
units: 3, batches: 1, from the cache: 0
translate() throws only for unusable input, a provider that fails completely or FallbackStrategy::Throw; all
exceptions implement HtmlTranslator\Exception\TranslatorException. See the usage guide.
Providers
| Provider | Configuration |
|---|---|
| OpenAI | OpenAiCompatibleConfig::openAi($apiKey, $model) |
| Gemini | OpenAiCompatibleConfig::gemini($apiKey, $model) |
| Ollama | OpenAiCompatibleConfig::ollama($model) |
| vLLM | OpenAiCompatibleConfig::vllm($model) |
| LM Studio | OpenAiCompatibleConfig::lmStudio($model) |
| Other OpenAI-compatible APIs | new OpenAiCompatibleConfig(...) |
| Anthropic | new AnthropicChatModel($http, $http, $http, new AnthropicConfig($model, $apiKey)) |
Documentation
- Usage guide: documents and fragments, what is translated, fallbacks, profiles, caching and costs, errors.
- Providers: configuration, JSON modes, timeouts, retries and errors of each adapter.
- Translation profiles: domain, formality, glossary and term policy.
- Architecture and decisions.
- Changelog.
Framework integrations
- Laravel:
mtubis/html-translator-laravel(Packagist) — providers and profiles inconfig/html-translator.php, theHtmlTranslationfacade, Artisan commands, Laravel's HTTP client, cache stores and log channels, andHtmlTranslation::fake()for tests. - Symfony bundle — planned.
Contributing
See CONTRIBUTING.md. Design decisions are recorded in docs/adr.
Tests against real models run with composer test:integration (configure .env from .env.dist), and
tools/eval compares models over the golden fixtures.
License
MIT