A Laravel package to synchronize translations between your Laravel application and a centralized translation management system. Pull translations from the server or push local translations back to keep everything in sync.
- 🚀 One Command Install - Get started in seconds
- 🔄 Bi-directional Sync - Pull and push translations with simple commands
- 🌍 Multi-language - Support for all languages
- 📦 Laravel Compliant - Generates proper Laravel translation files with nested arrays
- ⚡ CI/CD Ready - Perfect for automated deployments
- 🔒 Secure - API token authentication
- 🎯 Smart Filtering - Filter by language, status, or specific files
- ⬆️ Push Support - Send local translations back to the server
- 🔎 Source-Code Sync -
translations:missingandtranslations:cleanupreconcile the catalog with the actual$t()/trans()/__()/@langusage in your code - 🏠 Centralized Scan Config - Scan settings live on the project so every developer uses the same patterns (local
.envstill wins)
- PHP 8.1 or higher
- Laravel 10, 11, 12, or 13
Install via Composer:
composer require smartness/translation-clientThe package will automatically register itself via Laravel's package auto-discovery.
php artisan vendor:publish --tag=translation-client-configThis creates a config/translation-client.php file where you can customize settings.
Add these variables to your .env file:
# Required: Your API token
TRANSLATION_API_TOKEN=your_api_token_here
# Required: API endpoint URL
TRANSLATION_API_URL=https://your-translation-service.com/api
# Optional: Override default settings
TRANSLATION_OUTPUT_DIR= # Default: lang_path()
TRANSLATION_FORMAT=php # Options: php, json, raw
TRANSLATION_STATUS=approved # Filter: approved, pending, rejected
TRANSLATION_TIMEOUT=30 # HTTP timeout in seconds
# Optional: Source-scanning overrides (used by translations:cleanup and translations:missing).
# These are fetched from the server's project config by default, so most teams
# don't need to set them. Local values always override the server config.
SMARTPMS_TRANSLATION_SCAN_DIRS="resources,app"
SMARTPMS_TRANSLATION_SCAN_EXTENSIONS="php,blade.php,ts,tsx,js,jsx,vue"
SMARTPMS_TRANSLATION_KEY_PATTERN=
SMARTPMS_TRANSLATION_PREFIX_PATTERN=Note: You'll receive your API token and endpoint URL from your translation service administrator.
Pull all translations from the server:
php artisan translations:pullPull translations for a specific language:
php artisan translations:pull --language=enPreview changes without saving (dry-run):
php artisan translations:pull --dry-runTest API connection:
php artisan translations:pull --test# Override format for this pull
php artisan translations:pull --format=json
# Override status filter
php artisan translations:pull --status=approved
# Combine multiple options
php artisan translations:pull --language=de --status=approved --dry-runPush all local translations to the server:
php artisan translations:pushPush translations for a specific language:
php artisan translations:push --language=enPush a specific translation file:
php artisan translations:push --language=en --file=messagesPreview without actually pushing:
php artisan translations:push --dry-runOverwrite existing translations on the server:
php artisan translations:push --overwriteUse a custom translation directory:
php artisan translations:push --dir=/path/to/translations# Combine multiple options
php artisan translations:push --language=en --file=auth --overwrite
# Preview what will be pushed
php artisan translations:push --dry-run --language=deThese two commands scan your local source for $t('…'), useTranslate('…'), i18n.t('…'), trans('…'), __('…') and @lang('…') calls and reconcile what they find with what the server stores.
Both commands first fetch the project's central scan configuration from GET /translation-projects/config. Only the regex patterns (scan_key_pattern, scan_prefix_pattern) can be shared from the server; scan_dirs and scan_extensions are taken from local config or the package defaults only (never the server), since they control which local files are read. Local config always wins. Scan directories are also confined to the application base path. The resolved config is printed at the start of each run so you can verify which source provided each value.
# Dry-run: report keys referenced in source that don't exist on the server.
php artisan translations:missing
# Create those keys as placeholder rows on the project's primary language
# (value=null, missing=true, is_new=true), so they show up in the New Keys UI.
php artisan translations:missing --insertSample output:
Scan configuration:
scan_dirs (server ) resources, app
scan_extensions (server ) php, blade.php, ts, tsx, vue
key_pattern (default) (?:^|[^\w$])(?:\$?t|useTranslat(?:e|ion)|i18n…
prefix_pattern (default) (?:^|[^\w$])(?:\$?t|useTranslat(?:e|ion)|i18n…
Scanned 412 files.
Found 837 unique used key(s).
Remote keys: 540
Used keys sent: 837
Missing keys: 12
- dashboard.banner.welcome
- dashboard.banner.dismiss
…
# Dry-run: report stale keys.
php artisan translations:cleanup
# Actually delete them from the remote project.
php artisan translations:cleanup --deleteDynamic keys built with template literals (e.g. $t(`amenities.${name}`)) contribute a static prefix that protects every matching remote key from deletion — no false positives when the key set is computed at runtime.
The package creates translation files following Laravel's standard structure:
lang/
├── en/
│ ├── messages.php
│ ├── validation.php
│ └── auth.php
├── de/
│ ├── messages.php
│ ├── validation.php
│ └── auth.php
└── es/
├── messages.php
├── validation.php
└── auth.php
<?php
// lang/en/auth.php
return [
'failed' => 'These credentials do not match our records.',
'password' => 'The provided password is incorrect.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
// Nested arrays for dot-notation keys
'verification' => [
'sent' => 'A fresh verification link has been sent to your email address.',
'verified' => 'Your email address has been verified.',
],
];You can use the Translation Client directly in your code:
<?php
namespace App\Services;
use Smartness\TranslationClient\TranslationClient;
class TranslationSync
{
public function __construct(
protected TranslationClient $client
) {}
// Pull translations
public function syncTranslations(string $language): array
{
// Fetch translations as Laravel PHP arrays
$response = $this->client->fetchAsPhp($language);
return $response['data'];
}
public function syncAsJson(string $language): array
{
// Fetch translations as JSON format
$response = $this->client->fetchAsJson($language);
return $response['data'];
}
public function getRawTranslations(string $language): array
{
// Fetch raw format with metadata
$response = $this->client->fetchRaw($language);
return $response['data'];
}
// Push translations
public function pushTranslations(array $translations, bool $overwrite = false): array
{
// Push all translations
return $this->client->push($translations, [
'overwrite' => $overwrite,
]);
}
public function pushLanguageTranslations(string $language, array $translations, bool $overwrite = false): array
{
// Push translations for a specific language
return $this->client->pushLanguage($language, $translations, $overwrite);
}
public function pushFileTranslations(string $language, string $filename, array $translations): array
{
// Push a specific translation file
return $this->client->pushFile($language, $filename, $translations);
}
public function verifyConnection(): bool
{
return $this->client->testConnection();
}
}| Method | Description | Returns |
|---|---|---|
fetchAsPhp(?string $language) |
Fetch translations as nested PHP arrays (Laravel format) | array |
fetchAsJson(?string $language) |
Fetch translations as flat JSON structure | array |
fetchRaw(?string $language) |
Fetch raw format with full metadata | array |
fetch(array $options) |
Fetch with custom options | array |
push(array $translations, array $options) |
Push translations to the server | array |
pushLanguage(string $language, array $translations, bool $overwrite) |
Push translations for a specific language | array |
pushFile(string $language, string $filename, array $translations, bool $overwrite) |
Push a specific translation file | array |
fetchProjectConfig() |
Fetch the centralized scan config from the server. Returns null on failure so callers can fall back to local config. |
?array |
cleanup(array $usedKeys, array $usedPrefixes, bool $delete) |
Report (and optionally delete) remote keys not referenced in source. | array |
discoverMissing(array $usedKeys, array $usedPrefixes, bool $insert) |
Report (and optionally insert) keys referenced in source but missing remotely. | array |
testConnection() |
Verify API connection and token | bool |
Add this workflow to automatically sync translations:
# .github/workflows/sync-translations.yml
name: Sync Translations
on:
schedule:
- cron: '0 */6 * * *' # Every 6 hours
workflow_dispatch: # Allow manual trigger
jobs:
sync-translations:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
- name: Pull translations
env:
TRANSLATION_API_TOKEN: ${{ secrets.TRANSLATION_API_TOKEN }}
TRANSLATION_API_URL: ${{ secrets.TRANSLATION_API_URL }}
run: php artisan translations:pull
- name: Commit and push changes
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add lang/
if git diff --staged --quiet; then
echo "No translation changes"
else
git commit -m "chore: update translations [skip ci]"
git push
fi# .gitlab-ci.yml
sync-translations:
stage: deploy
script:
- composer install --no-dev --optimize-autoloader
- php artisan translations:pull
- git config user.name "GitLab CI"
- git config user.email "ci@gitlab.com"
- git add lang/
- git diff --staged --quiet || git commit -m "chore: update translations [skip ci]"
- git push https://${GITLAB_USER}:${GITLAB_TOKEN}@${CI_REPOSITORY_URL#*@}
only:
- schedules
variables:
TRANSLATION_API_TOKEN: $TRANSLATION_API_TOKEN
TRANSLATION_API_URL: $TRANSLATION_API_URLAll configuration options available in config/translation-client.php:
return [
// API endpoint (required)
'api_url' => env('TRANSLATION_API_URL'),
// API authentication token (required)
'api_token' => env('TRANSLATION_API_TOKEN'),
// Output directory for translation files
// Default: lang_path() resolves to 'lang/' directory
'output_dir' => env('TRANSLATION_OUTPUT_DIR', null),
// Format: json, php, or raw
'format' => env('TRANSLATION_FORMAT', 'php'),
// Status filter: approved, pending, rejected, or null (all)
'status_filter' => env('TRANSLATION_STATUS', 'approved'),
// HTTP request timeout in seconds
'timeout' => env('TRANSLATION_TIMEOUT', 30),
];The package provides clear, actionable error messages:
# Missing configuration
❌ API token not configured. Please set TRANSLATION_API_TOKEN in your .env file.
# Invalid credentials
❌ Authentication failed: Invalid API token. Please check your TRANSLATION_API_TOKEN configuration.
# Connection issues
❌ API error: Failed to connect to translation service: Connection timeout
# No translations found
⚠ No translations found matching the criteria.Solution: Add your API token to .env:
TRANSLATION_API_TOKEN=your_token_here
TRANSLATION_API_URL=https://your-service.com/apiSolution: Verify your API token is correct. Contact your translation service administrator if needed.
Solutions:
- Check your network connection
- Verify the API URL is correct
- Increase timeout:
TRANSLATION_TIMEOUT=60
Solutions:
- Run with
--dry-runto preview changes - Check status filter:
--status=approved - Verify you have translations in the system
use Smartness\TranslationClient\TranslationClient;
$client = app(TranslationClient::class);
$response = $client->fetch([
'format' => 'php',
'language' => 'en',
'status' => 'approved',
'missing' => false,
'filename' => 'messages',
]);# Save to different directory
TRANSLATION_OUTPUT_DIR=/path/to/custom/lang# Development
TRANSLATION_API_URL=https://dev-translation-service.com/api
# Production
TRANSLATION_API_URL=https://translation-service.com/api- ✅ API token authentication
- ✅ HTTPS required for API communication
- ✅ Token validation before requests
- ✅ No sensitive data in logs
Important: Never commit your API token to version control. Always use environment variables.
For issues, questions, or feature requests:
- Email: dev@smartpricing.com
- Issues: GitHub Issues
The MIT License (MIT). Please see License File for more information.