morphMany(Translation::class, 'translatable');
}
/**
* Get translation for a specific field and language
*/
public function getTranslation(string $fieldName, ?string $languageCode = null, bool $published = true): ?Translation
{
$languageCode = $languageCode ?? $this->getCurrentLanguageCode();
$query = $this->translations()
->where('field_name', $fieldName)
->where('language_code', $languageCode);
if ($published) {
$query->where('status', 'published');
}
return $query->first();
}
/**
* Get translation value for a specific field and language
*/
public function translate(string $fieldName, ?string $languageCode = null, bool $fallback = true): mixed
{
$languageCode = $languageCode ?? $this->getCurrentLanguageCode();
// 1. Try translations table
$translation = $this->getTranslation($fieldName, $languageCode);
if ($translation && $this->hasValidTranslationContent($translation->field_value)) {
return $translation->field_value;
}
// 2. Fallback to default language from translations table
if ($fallback && $languageCode !== $this->getDefaultLanguageCode()) {
$defaultTranslation = $this->getTranslation($fieldName, $this->getDefaultLanguageCode());
if ($defaultTranslation && $this->hasValidTranslationContent($defaultTranslation->field_value)) {
return $defaultTranslation->field_value;
}
}
// 3. Fallback to original field value
$originalValue = $this->{$fieldName} ?? null;
// Handle JSON string if not casted
if (is_string($originalValue) && (str_starts_with($originalValue, '{') || str_starts_with($originalValue, '['))) {
$decoded = json_decode($originalValue, true);
if (json_last_error() === JSON_ERROR_NONE) {
$originalValue = $decoded;
}
}
// If the original value is an array (e.g. JSON cast), try to find translation inside it
if (is_array($originalValue)) {
if (isset($originalValue[$languageCode]) && !empty($originalValue[$languageCode])) {
return $originalValue[$languageCode];
}
if ($fallback && isset($originalValue[$this->getDefaultLanguageCode()]) && !empty($originalValue[$this->getDefaultLanguageCode()])) {
return $originalValue[$this->getDefaultLanguageCode()];
}
// Return first non-empty value if available
foreach ($originalValue as $val) {
if (!empty($val)) {
return $val;
}
}
return '';
}
return $originalValue;
}
/**
* Set translation for a specific field and language
*/
public function setTranslation(
string $fieldName,
string $languageCode,
mixed $value,
string $status = 'draft',
?int $userId = null
): Translation {
$userId = $userId ?? auth()->id();
return $this->translations()->updateOrCreate(
[
'field_name' => $fieldName,
'language_code' => $languageCode,
],
[
'field_value' => $value,
'status' => $status,
'created_by' => $userId,
'updated_by' => $userId,
]
);
}
/**
* Get all translations for a specific field
*/
public function getTranslations(string $fieldName, bool $published = true): Collection
{
$query = $this->translations()
->where('field_name', $fieldName);
if ($published) {
$query->where('status', 'published');
}
return $query->get()->keyBy('language_code');
}
/**
* Get all translations for a specific language
*/
public function getTranslationsForLanguage(string $languageCode, bool $published = true): Collection
{
$query = $this->translations()
->where('language_code', $languageCode);
if ($published) {
$query->where('status', 'published');
}
return $query->get()->keyBy('field_name');
}
/**
* Check if translation exists for a field and language
*/
public function hasTranslation(string $fieldName, string $languageCode, bool $published = true): bool
{
$query = $this->translations()
->where('field_name', $fieldName)
->where('language_code', $languageCode);
if ($published) {
$query->where('status', 'published');
}
return $query->exists();
}
/**
* Get translation progress for all languages
*/
public function getTranslationProgress(): array
{
$languages = Language::active()->get();
$translatableFields = $this->getTranslatableFields();
$totalFields = count($translatableFields);
$progress = [];
foreach ($languages as $language) {
$translatedCount = $this->translations()
->where('language_code', $language->code)
->where('status', 'published')
->whereIn('field_name', $translatableFields)
->count();
$progress[$language->code] = $totalFields > 0
? round(($translatedCount / $totalFields) * 100)
: 0;
}
return $progress;
}
/**
* Get missing translations for a specific language
*/
public function getMissingTranslations(string $languageCode): array
{
$translatableFields = $this->getTranslatableFields();
$existingTranslations = $this->translations()
->where('language_code', $languageCode)
->where('status', 'published')
->pluck('field_name')
->toArray();
return array_diff($translatableFields, $existingTranslations);
}
/**
* Duplicate translation from one language to another
*/
public function duplicateTranslation(string $fromLanguage, string $toLanguage, ?int $userId = null): int
{
$userId = $userId ?? auth()->id();
$count = 0;
$sourceTranslations = $this->getTranslationsForLanguage($fromLanguage, false);
foreach ($sourceTranslations as $fieldName => $translation) {
$this->setTranslation(
$fieldName,
$toLanguage,
$translation->field_value,
'draft',
$userId
);
$count++;
}
return $count;
}
/**
* Get translation status for a language
*/
public function getTranslationStatus(string $languageCode): string
{
$progress = $this->getTranslationProgress();
$percentage = $progress[$languageCode] ?? 0;
if ($percentage === 100) {
return 'complete';
} elseif ($percentage > 0) {
return 'partial';
} else {
return 'missing';
}
}
/**
* Sync translations for all translatable fields
*/
public function syncTranslations(array $languageCodes, ?int $userId = null): void
{
$userId = $userId ?? auth()->id();
$translatableFields = $this->getTranslatableFields();
foreach ($languageCodes as $languageCode) {
foreach ($translatableFields as $field) {
if (!$this->hasTranslation($field, $languageCode, false)) {
$this->setTranslation(
$field,
$languageCode,
$this->{$field} ?? '',
'draft',
$userId
);
}
}
}
}
/**
* Check if translation content has meaningful text or media
*/
protected function hasValidTranslationContent(mixed $value): bool
{
if (empty($value)) {
return false;
}
if (is_string($value)) {
$stripped = trim(strip_tags($value, '![]()