Skip to main content

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 $modelClass pointing to a model
  • The model must use the HasTranslation trait
  • 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',
];
}
}
Important

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

  1. Always call parent constructor when overriding the constructor
  2. Set defaults in constructor, not as class property
  3. Use array_key_exists when checking for translation in update operations
  4. Ensure translations relation in query hooks for consistent eager loading

See Also