HasServiceTranslation Trait
The HasServiceTranslation trait provides reusable translation management methods for CRUD services. It automatically reads translatable fields from your model's $translatables property and provides methods to sync and normalize translation payloads.
When to Use HasServiceTranslation
Use HasServiceTranslation when you need:
- Service-layer translation management: Manage translations in your service classes instead of directly in models
- Automatic field detection: Automatically read translatable fields from your model class
- Payload normalization: Normalize translation payloads for store and update operations
- Multiple format support: Handle both locale-keyed and single-locale translation formats
- Default values: Set default values for translatable fields on store operations
- Eager loading support: Easily ensure translations relation is loaded
Example scenarios:
- Building CRUD services for translatable entities
- Creating API endpoints that handle multilingual content
- Building admin panels with translation support
- Implementing bulk translation operations
Overview
HasServiceTranslation is designed to work alongside the HasTranslation trait on models. While HasTranslation provides the model-level translation capabilities, HasServiceTranslation provides the service-level utilities for managing those translations.
Namespace
JobMetric\Translation\HasServiceTranslation
Requirements
- Service must define
protected static string $modelClasspointing to a model - The model must use the
HasTranslationtrait - The model must define
protected array $translatables
Quick Start
Add the trait to your service class:
<?php
namespace App\Services;
use App\Models\Product;
use Illuminate\Database\Eloquent\Model;
use JobMetric\PackageCore\Services\AbstractCrudService;
use JobMetric\Translation\HasServiceTranslation;
class ProductService extends AbstractCrudService
{
use HasServiceTranslation;
protected static string $modelClass = Product::class;
/**
* Hook called after storing a model.
*/
protected function afterStore(Model $model, array &$data): void
{
$this->syncTranslations($model, $data['translation'] ?? null, false);
}
/**
* Hook called after updating a model.
*/
protected function afterUpdate(Model $model, array &$data): void
{
if (array_key_exists('translation', $data)) {
$this->syncTranslations($model, $data['translation'], true);
}
}
}
Configuration Properties
$translationDefaults
Set default values for translatable fields during store operations. This is useful when you want certain fields to have default values if not provided.
class ProductService extends AbstractCrudService
{
use HasServiceTranslation;
protected static string $modelClass = Product::class;
public function __construct()
{
parent::__construct();
// Set default values for translatable fields
$this->translationDefaults = [
'status' => 'draft',
'position' => 'left',
];
}
}
Do not override $translationDefaults as a class property directly, as it will conflict with the trait's property. Instead, set it in the constructor.
Available Methods
getTranslatableFields()
Returns the translatable fields defined in the model. Results are cached statically per model class for performance.
protected function getTranslatableFields(): array
Returns: Array of translatable field names, or empty array if wildcard ['*'] is defined.
Example:
$fields = $this->getTranslatableFields();
// Returns: ['name', 'description', 'summary']
ensureTranslationsRelation()
Ensures the translations relation is present in an eager-load array. This is useful when building queries.
protected function ensureTranslationsRelation(array $with): array
Parameters:
$with- Array of relations to eager-load
Returns: The array with translations added if not already present.
Example:
$with = $this->ensureTranslationsRelation(['category', 'tags']);
// Returns: ['category', 'tags', 'translations']
$with = $this->ensureTranslationsRelation(['translations', 'category']);
// Returns: ['translations', 'category'] (no duplicate)
syncTranslations()
Upserts translations for a model. Supports both locale-keyed and single-locale formats.
protected function syncTranslations(Model $model, mixed $translation, bool $isUpdate): void
Parameters:
$model- The Eloquent model instance$translation- Translation data (array or null)$isUpdate- True for update operations, false for store operations
Supported Formats:
Locale-keyed format:
$this->syncTranslations($product, [
'en' => ['name' => 'Product Name', 'description' => 'Description'],
'fa' => ['name' => 'نام محصول', 'description' => 'توضیحات'],
], false);
Single-locale (legacy) format:
// Uses current application locale
$this->syncTranslations($product, [
'name' => 'Product Name',
'description' => 'Description',
], false);
normalizeTranslationPayload()
Normalizes translation input into a payload suitable for the model's translate() method.
protected function normalizeTranslationPayload(array $fields, bool $isUpdate): array
Parameters:
$fields- Input fields from the request$isUpdate- True for update, false for store
Behavior:
- On store (
$isUpdate = false): Includes all translatable fields with defaults - On update (
$isUpdate = true): Only includes fields that are present in input
Example:
// Model has translatables: ['name', 'description', 'status']
// Defaults: ['status' => 'draft']
// On store
$payload = $this->normalizeTranslationPayload(['name' => 'Test'], false);
// Returns: ['name' => 'Test', 'description' => null, 'status' => 'draft']
// On update
$payload = $this->normalizeTranslationPayload(['name' => 'Updated'], true);
// Returns: ['name' => 'Updated']
setTranslationDefaults()
Dynamically set default values for translatable fields.
public function setTranslationDefaults(array $defaults): static
Parameters:
$defaults- Associative array of field => default value
Returns: The service instance (fluent interface)
Example:
$service->setTranslationDefaults([
'status' => 'active',
'position' => 'right',
])->store($data);
getTranslationDefault()
Get the default value for a specific translatable field.
protected function getTranslationDefault(string $field): mixed
Parameters:
$field- Field name
Returns: The default value or null if not set.
clearTranslatableFieldsCache()
Clears the static cache for translatable fields. Useful for testing or when model configuration changes at runtime.
public static function clearTranslatableFieldsCache(): void
Example:
ProductService::clearTranslatableFieldsCache();
Complete Example
Here's a complete example of a service using HasServiceTranslation:
<?php
namespace JobMetric\UnitConverter;
use Illuminate\Database\Eloquent\Model;
use JobMetric\PackageCore\Services\AbstractCrudService;
use JobMetric\Translation\HasServiceTranslation;
use JobMetric\UnitConverter\Models\Unit;
use JobMetric\UnitConverter\Http\Resources\UnitResource;
class UnitConverter extends AbstractCrudService
{
use HasServiceTranslation;
protected string $entityName = 'unit::base.entity_names.unit';
protected static string $modelClass = Unit::class;
protected static string $resourceClass = UnitResource::class;
public function __construct()
{
parent::__construct();
// Set default for 'position' field
$this->translationDefaults = [
'position' => 'left',
];
}
/**
* Store hook: sync translations after creating the model.
*/
protected function afterStore(Model $model, array &$data): void
{
$this->syncTranslations($model, $data['translation'] ?? null, false);
}
/**
* Update hook: sync translations if provided.
*/
protected function afterUpdate(Model $model, array &$data): void
{
if (array_key_exists('translation', $data)) {
$this->syncTranslations($model, $data['translation'], true);
}
}
/**
* Query hook: ensure translations are always eager-loaded.
*/
protected function afterQuery($qb, array $filters, array &$with, ?string $mode): void
{
$with = $this->ensureTranslationsRelation($with);
}
/**
* Show hook: ensure translations are loaded for single item.
*/
protected function afterShow(Model $model, array &$with): void
{
$with = $this->ensureTranslationsRelation($with);
}
}
Format Detection
The trait automatically detects whether the translation input is in locale-keyed format or single-locale format:
Locale-keyed format is detected when:
- The first key is a string with 2-5 characters (like locale codes:
en,fa,ar,zh-CN) - The first value is an array
- The first key is not a translatable field name
Single-locale format is assumed otherwise, and the current application locale is used.
// Locale-keyed (detected automatically)
[
'en' => ['name' => 'English Name'],
'fa' => ['name' => 'نام فارسی'],
]
// Single-locale (uses app()->getLocale())
[
'name' => 'Product Name',
'description' => 'Description',
]
Caching
Translatable fields are cached statically per model class. This means:
- Fields are only resolved once per request, regardless of how many service instances are created
- Cache is shared across all instances of the same service
- Use
clearTranslatableFieldsCache()to clear the cache if needed
Best Practices
- Always call parent constructor when overriding the constructor
- Set defaults in constructor, not as class property
- Use
array_key_existswhen checking for translation in update operations - Ensure translations relation in query hooks for consistent eager loading
See Also
- HasTranslation Trait - Model-level translation trait
- Translation Service - Shared service entry point for set-translation workflows
- Translation Events - Events fired during translation operations