wazza / dom-translate
A Laravel Package that will use the build-in Blade Directive to define specific phrases for auto-translation before being rendered to the screen.
Requires
- php: ^8.2
- ext-json: *
- guzzlehttp/guzzle: ^7.8
- illuminate/support: ^12.0
Requires (Dev)
- fakerphp/faker: ^1.20.0
- mockery/mockery: ^1.2
- orchestra/testbench: ^10.0
- pestphp/pest: ^3.8
- pestphp/pest-plugin-laravel: ^3.2
- phpunit/phpunit: ^11.0
README
Laravel Translate Package
A Laravel package that uses Blade directives to provide real-time translations for any phrase in your views — backed by a smart caching layer (session → database → cloud API) to minimise API calls and costs.
Write your Blade templates in English, serve every visitor in their own language — automatically.
Table of Contents
- How it Works
- Requirements
- Installation
- Configuration
- Translation Providers
- Blade Directives
- Auto-Translation (
@transl8auto) - Language Preference API Routes
- SetLocale Middleware
- Using the Helper Directly
- Upgrading to v2.5
- Troubleshooting
- Contributing
How it Works
Three database tables (domt_languages, domt_phrases, domt_translations) power a three-level cache:
- Session (fastest) — if
DOM_TRANSLATE_USE_SESSION=true, translations are stored in the user's session after the first retrieval. - Database (fast) — if
DOM_TRANSLATE_USE_DATABASE=true(default), translations are stored and retrieved from the DB. Each phrase is hashed (SHA-256 HMAC) and stored in an indexed column for sub-millisecond lookups. - Cloud API (fallback) — only when no cached translation exists. The result is immediately stored in the DB (and session) to prevent future API calls.
Laravel caches compiled Blade views, so unchanged pages make zero database or API calls after the first render.
Requirements
| Requirement | Version |
|---|---|
| PHP | ^8.2 |
| Laravel | ^12.0 |
| GuzzleHTTP | ^7.8 (auto-installed) |
| Cloud API key | Google or Bing (at least one) |
Installation
composer require wazza/dom-translate
Publish the config and migrations, then run the migrations:
php artisan vendor:publish --tag="dom-translate-config" php artisan vendor:publish --tag="dom-translate-migrations" php artisan migrate
Add your API key(s) to .env:
DOM_TRANSLATE_GOOGLE_KEY=your_google_key_here # or DOM_TRANSLATE_BING_KEY=your_bing_key_here DOM_TRANSLATE_PROVIDER=bing
Clear config cache:
php artisan config:clear
Auto-discovery: The service provider is auto-discovered. No manual registration is needed in
bootstrap/providers.phpunless you have disabled package discovery.
Configuration
After publishing, the config file is at config/dom_translate.php. All settings are also controllable via .env:
# Caching layers DOM_TRANSLATE_USE_SESSION=false # Cache translations in the user's session DOM_TRANSLATE_USE_DATABASE=true # Cache translations in the database (recommended) # Translation provider DOM_TRANSLATE_PROVIDER=google # 'google' or 'bing' DOM_TRANSLATE_GOOGLE_KEY= # Google Cloud Translation API v2 key DOM_TRANSLATE_BING_KEY= # Azure Cognitive Services subscription key # Languages DOM_TRANSLATE_LANG_SRC=en # Default source language (ISO 639-1) DOM_TRANSLATE_LANG_DEST=af # Default destination language (ISO 639-1) # Phrase hashing DOM_TRANSLATE_HASH_SALT=DzBQ2DxKhNaF # Change this — used in HMAC hash DOM_TRANSLATE_HASH_ALGO=sha256 # Logging (0=off, 1=high, 2=mid, 3=verbose) DOM_TRANSLATE_LOG_LEVEL=1 DOM_TRANSLATE_LOG_INDICATOR=dom-translate # Language preference routes DOM_TRANSLATE_ROUTES_ENABLED=true DOM_TRANSLATE_ROUTES_PREFIX=api/translate DOM_TRANSLATE_ROUTES_MIDDLEWARE=web # Session/cookie key for language preference DOM_TRANSLATE_SESSION_KEY=app_language_code # SetLocale middleware DOM_TRANSLATE_MIDDLEWARE_ENABLED=true DOM_TRANSLATE_MIDDLEWARE_AUTO_APPLY=true
Security note: Change
DOM_TRANSLATE_HASH_SALTto a unique value per application. Do not reuse the default across projects.
Translation Providers
Google Cloud Translation Setup
- Visit Google Cloud Console and create a project.
- Enable Cloud Translation API under APIs & Services → Library.
- Create an API key under APIs & Services → Credentials.
- Restrict the key to Cloud Translation API to limit exposure.
- Add to
.env:DOM_TRANSLATE_PROVIDER=google DOM_TRANSLATE_GOOGLE_KEY=your_key_here
Free tier: 500,000 characters/month. Billing account required even for free tier.
Test with curl:
curl -X POST \ "https://www.googleapis.com/language/translate/v2?key=YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"Hello world","target":"fr","source":"en"}'
Bing / Azure Cognitive Translator Setup
- Sign in to the Azure Portal.
- Create a Cognitive Services resource (or a dedicated Translator resource).
- Copy one of the subscription keys from Keys and Endpoint.
- Add to
.env:DOM_TRANSLATE_PROVIDER=bing DOM_TRANSLATE_BING_KEY=your_azure_key_here
Free tier: 2,000,000 characters/month (F0 tier).
Blade Directives
@transl8(phrase[, dest[, src]])
The primary directive. Translates a phrase to the destination language.
{{-- Use config defaults for src/dest languages --}} <p>@transl8("I like this feature.")</p> {{-- Specify destination --}} <p>@transl8("We need to test it in staging.", "de")</p> {{-- Specify both source and destination --}} <p>@transl8("Wie weet waar Willem Wouter woon?", "en", "af")</p>
Language-Specific Shortcuts
Pre-registered directives for common languages:
<p>@transl8fr("Translated to French")</p> <p>@transl8de("Translated to German")</p> <p>@transl8nl("Translated to Dutch")</p> <p>@transl8es("Translated to Spanish")</p> <p>@transl8it("Translated to Italian")</p> <p>@transl8pt("Translated to Portuguese")</p> <p>@transl8ru("Translated to Russian")</p> <p>@transl8zhcn("Translated to Chinese Simplified")</p> <p>@transl8zhtw("Translated to Chinese Traditional")</p> <p>@transl8af("Translated to Afrikaans")</p> <p>@transl8ar("Translated to Arabic")</p>
Adding more languages in your AppServiceProvider::register():
use Wazza\DomTranslate\Controllers\TranslateController; Blade::directive('transl8ja', function ($string) { return "<?= app(" . TranslateController::class . "::class)->translate({$string}, 'ja', 'en'); ?>"; });
@transl8auto(phrase)
Translates to whatever language is stored in the user's session or cookie. See Auto-Translation below.
<h1>@transl8auto("Welcome to our website!")</h1> <p>@transl8auto("This content adapts to the visitor's preferred language.")</p>
Auto-Translation (@transl8auto)
@transl8auto is the most powerful directive. It reads the user's language preference (set via a language switcher) and translates accordingly — no per-phrase language specification needed.
Language priority order
- Session —
session('app_language_code') - Cookie —
cookie('app_language_code') - Config default —
dom_translate.language.dest - App locale —
config('app.locale') - English — final fallback
Building a language switcher
1. Add a selector to your layout:
<select id="languageSelector"> <option value="en">English</option> <option value="fr">Français</option> <option value="de">Deutsch</option> <option value="es">Español</option> <option value="nl">Nederlands</option> <option value="af">Afrikaans</option> </select>
2. Wire it up with JavaScript:
const selector = document.getElementById('languageSelector'); // Restore current preference on load fetch('/api/translate/get-language') .then(r => r.json()) .then(data => { selector.value = data.language; }); // Persist change and reload selector.addEventListener('change', async function () { await fetch('/api/translate/set-language', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify({ language: this.value }) }); window.location.reload(); });
Vue 3 Component Example
<template> <select v-model="language" @change="switchLanguage"> <option value="en">English</option> <option value="fr">Français</option> <option value="de">Deutsch</option> <option value="es">Español</option> </select> </template> <script setup> import { ref, onMounted } from 'vue' import axios from 'axios' const language = ref('en') onMounted(async () => { const { data } = await axios.get('/api/translate/get-language') language.value = data.language }) async function switchLanguage() { await axios.post('/api/translate/set-language', { language: language.value }) window.location.reload() } </script>
Language Preference API Routes
Enabled by default. Can be disabled with DOM_TRANSLATE_ROUTES_ENABLED=false.
POST /api/translate/set-language
POST /api/translate/set-language Content-Type: application/json { "language": "fr" }
Response:
{
"message": "Language preference set successfully.",
"language": "fr"
}
Also sets a 1-year, httpOnly cookie with the preference.
GET /api/translate/get-language
GET /api/translate/get-language
Response:
{ "language": "fr" }
Disable routes and roll your own
DOM_TRANSLATE_ROUTES_ENABLED=false
use Wazza\DomTranslate\Helpers\TranslateHelper; class LanguageController extends Controller { public function set(Request $request) { $request->validate(['language' => 'required|string|min:2|max:5']); return TranslateHelper::setLanguage($request->input('language')); } public function get() { return TranslateHelper::getLanguage(); } }
SetLocale Middleware
SetLocaleMiddleware automatically syncs Laravel's application locale — and Carbon's locale — to the user's language preference on every request.
This ensures __(), trans(), @lang, validation messages, and Carbon date formatting all use the same language as the translate directives.
Enabled by default. To disable:
DOM_TRANSLATE_MIDDLEWARE_ENABLED=false
To keep the middleware but not auto-apply it to the web group:
DOM_TRANSLATE_MIDDLEWARE_AUTO_APPLY=false
Then apply it manually where needed:
// routes/web.php Route::middleware(['web', 'dom-translate.locale'])->group(function () { // your routes });
Using the Helper Directly
TranslateHelper and PhraseHelper are also available for use in controllers, jobs, or anywhere outside Blade:
use Wazza\DomTranslate\Helpers\TranslateHelper; use Wazza\DomTranslate\Controllers\TranslateController; // Translate directly $translated = app(TranslateController::class)->translate('Hello world', 'fr', 'en'); // Auto-translate using current user's preference $translated = TranslateHelper::autoTransl8('Hello world'); // Get current user's language code $lang = TranslateHelper::currentDefinedLanguageCode(); // Set language preference (returns JsonResponse with cookie) return TranslateHelper::setLanguage('de');
Upgrading to v2.5
If you are upgrading from v2.4.x:
-
Re-publish migrations — the migration files have been updated to anonymous class syntax (required by Laravel 12). If you have already run the old migrations, no action is needed for existing databases. For fresh installs, re-publish:
php artisan vendor:publish --tag="dom-translate-migrations" --force php artisan migrate -
No breaking changes — all Blade directives, config keys, and
.envvariables remain identical. -
BingTranslate is now implemented — if you set
DOM_TRANSLATE_PROVIDER=bingin your.env, it will now work with the Azure Cognitive Translator v3 API. -
Cookie security — the language preference cookie is now
httpOnly=true(XSS protection). This is transparent to your application. -
DOM_TRANSLATE_HASH_SALT— if you are using a custom salt, ensure it remains set in your.envto preserve existing hash lookups in the database.
Troubleshooting
E_STRICT deprecation warnings during composer install
Deprecation Notice: Constant E_STRICT is deprecated in /usr/share/php/Composer/Util/Silencer.php
This is not caused by this package. It is caused by an outdated system-level Composer installation (typically /usr/share/php/Composer/). Update your server's Composer:
sudo composer self-update
# or, on Debian/Ubuntu:
sudo apt-get install --only-upgrade composer
Translations not showing / returning original phrase
- Check your API key is set:
DOM_TRANSLATE_GOOGLE_KEYorDOM_TRANSLATE_BING_KEY. - Check
DOM_TRANSLATE_LOG_LEVEL=3and review Laravel's log for API errors. - Ensure the destination language code (
DOM_TRANSLATE_LANG_DEST) exists in thedomt_languagestable. - Clear config and view cache:
php artisan config:clear php artisan view:clear
MissingAppKeyException in tests
Ensure APP_KEY is set in phpunit.xml or your .env.testing. The package's test suite sets a dummy key automatically for its own tests.
Language not persisting between requests
Ensure DOM_TRANSLATE_USE_SESSION=true or that cookies are being sent with requests. For AJAX requests, verify the X-CSRF-TOKEN header is included in POST calls.
Contributing
Contributions are welcome. Please read CONTRIBUTING.md before opening a pull request. For security vulnerabilities, see SECURITY.md.
Made with ☕ by Warren Coetzee