| Current Path : /var/www/html/redd/docs/ |
| Current File : /var/www/html/redd/docs/TRANSLATION-GUIDE.md |
# Panduan Translation Content
## Ringkasan
Sistem guna dua pattern translation:
| Pattern | Module | Cara Kerja |
|---------|--------|------------|
| **Separate table** | ContentArticle, ContentApplication, ContentSlider, ContentVideo | Data translatable disimpan dalam table berasingan, di-link via `*_translation_parent_id` FK |
| **Flat sibling** | ContentDownload, ContentImage, ContentPhotoGallery, ContentPhotoList | Translation adalah row lain dalam table yang sama, di-link via `*_parent_id`, dengan `*_main = 1` untuk parent |
Kedua-dua pattern menggunakan konsep **parent + siblings**, bezanya cuma lokasi simpanan data.
---
## 1. Setup Middleware (dah siap)
`app/Http/Middleware/SetLocale.php` — auto set `app()->getLocale()` dari session.
Didaftarkan di `bootstrap/app.php` sebagai alias `'locale'` dan guna di routes public.
---
## 2. Tukar Bahasa
```
GET /lang/{locale}
```
Contoh: `/lang/en` atau `/lang/ms`
Simpan locale dalam session, redirect balik.
---
## 3. Display Content Ikut Bahasa
### Pattern: Separate Translation Table
Sesuai untuk: **ContentArticle**, **ContentApplication**, **ContentSlider**, **ContentVideo**
```php
use App\Models\Backend\ContentArticle;
$locale = app()->getLocale();
// Single item
$article = ContentArticle::with(['translations' => function ($q) use ($locale) {
$q->where('article_translation_language', $locale);
}])->findOrFail($id);
// List
$articles = ContentArticle::with(['translations' => function ($q) use ($locale) {
$q->where('article_translation_language', $locale);
}])->get();
```
View:
```blade
@php
$translation = $article->translations->first();
@endphp
<h1>{{ $translation->article_translation_title ?? '-' }}</h1>
<div>{!! $translation->article_translation_content ?? '' !!}</div>
```
#### ContentArticle
| Table | Translation FK Field | Translation Language Field |
|-------|---------------------|--------------------------|
| `content_article` | `article_translation_parent_id` → `article_id` | `article_translation_language` |
| Translatable Column | Type |
|--------------------|------|
| `article_translation_title` | `text` |
| `article_translation_content` | `longtext` (Summernote) |
#### ContentApplication
| Table | Translation FK Field | Translation Language Field |
|-------|---------------------|--------------------------|
| `content_applications` | `application_translation_parent_id` → `application_id` | `application_translation_language` |
| Translatable Column | Type |
|--------------------|------|
| `application_translation_title` | `varchar(255)` |
```php
use App\Models\Backend\ContentApplication;
$locale = app()->getLocale();
$applications = ContentApplication::with(['translations' => function ($q) use ($locale) {
$q->where('application_translation_language', $locale);
}])->get();
```
View:
```blade
@php
$translation = $application->translations->first();
@endphp
<h3>{{ $translation->application_translation_title ?? '-' }}</h3>
```
#### ContentSlider
| Table | Translation FK Field | Translation Language Field |
|-------|---------------------|--------------------------|
| `content_slider` | `slider_translation_parent_id` → `slider_id` | `slider_translation_language` |
| Translatable Column | Type | Notes |
|--------------------|------|-------|
| `slider_translation_title` | `text` | |
| `slider_translation_img` | `text` | File path — berbeza mengikut bahasa |
```php
use App\Models\Backend\ContentSlider;
$locale = app()->getLocale();
$sliders = ContentSlider::with(['translations' => function ($q) use ($locale) {
$q->where('slider_translation_language', $locale);
}])->where('slider_status', 'ACTIVE')->orderBy('slider_sort')->get();
```
View:
```blade
@foreach ($sliders as $slider)
@php
$trans = $slider->translations->first();
@endphp
@if ($trans)
<img src="{{ asset('storage/' . $trans->slider_translation_img) }}"
alt="{{ $trans->slider_translation_title }}">
@endif
@endforeach
```
#### ContentVideo
| Table | Translation FK Field | Translation Language Field |
|-------|---------------------|--------------------------|
| `content_video` | `video_translation_parent_id` → `video_id` | `video_translation_language` |
| Translatable Column | Type |
|--------------------|------|
| `video_translation_title` | `text` |
| `video_translation_content` | `text` |
| `video_translation_location` | `text` |
| `video_translation_url` | `varchar(255)` |
```php
use App\Models\Backend\ContentVideo;
$locale = app()->getLocale();
$videos = ContentVideo::with(['translations' => function ($q) use ($locale) {
$q->where('video_translation_language', $locale);
}])->orderBy('video_sorting')->get();
```
View:
```blade
@foreach ($videos as $video)
@php
$trans = $video->translations->first();
@endphp
<h3>{{ $trans->video_translation_title ?? '-' }}</h3>
@if ($trans && $trans->video_translation_url)
<a href="{{ $trans->video_translation_url }}">Link</a>
@endif
@endforeach
```
---
### Pattern: Flat Sibling (Self-Referential)
Sesuai untuk: **ContentDownload**, **ContentImage**, **ContentPhotoGallery**, **ContentPhotoList**
Dalam pattern ini, parent row dan sibling translation **duduk dalam table yang sama**. Parent ditanda dengan `*_main = 1`, manakala sibling ada `*_main = 0` dan `*_parent_id` pointing ke parent.
Frontend query guna `WHERE *_main = 1 AND *_language = ?` — parent row yang bahasanya sepadan dengan locale semasa.
```php
use App\Models\Backend\ContentDownload;
$locale = app()->getLocale();
// Parent rows dalam locale semasa
$downloads = ContentDownload::where('download_main', 1)
->where('download_language', $locale)
->orderBy('created_at', 'desc')
->get();
```
View — parent row sudah ada data untuk locale tersebut:
```blade
@foreach ($downloads as $download)
<h3>{{ $download->download_title }}</h3>
@if ($download->download_file)
<a href="{{ asset('storage/' . $download->download_file) }}">Download</a>
@endif
@endforeach
```
#### ContentDownload
| Column Parent | Column Sibling |
|--------------|---------------|
| `download_main = 1` | `download_main = 0` |
| `download_parent_id = null` | `download_parent_id` → parent `download_id` |
| `download_language` = locale | `download_language` = locale |
| Translatable Column | Type |
|--------------------|------|
| `download_title` | `varchar(255)` |
| `download_img` | `varchar(255)` — file path, berbeza per language |
| `download_file` | `varchar(255)` — file path, berbeza per language |
```php
$downloads = ContentDownload::where('download_main', 1)
->where('download_language', app()->getLocale())
->orderBy('created_at', 'desc')
->get();
```
Fallback ke parent kalau field tertentu kosong (guna `translations()` relationship):
```blade
@php
// Kalau nak guna sibling dengan fallback:
$sibling = $download->translations->first(); // translation dalam locale lain
$title = $download->download_title; // parent title (locale semasa)
$file = $sibling->download_file ?? $download->download_file; // fallback
@endphp
```
#### ContentImage
| Column Parent | Column Sibling |
|--------------|---------------|
| `image_main = 1` | `image_main = 0` |
| `image_parent_id = null` | `image_parent_id` → parent `image_id` |
| `image_language` = locale | `image_language` = locale |
| Translatable Column | Type |
|--------------------|------|
| `image_title` | `varchar(255)` |
| `image_file` | `varchar(255)` — file path, berbeza per language |
```php
use App\Models\Backend\ContentImage;
$locale = app()->getLocale();
$images = ContentImage::where('image_main', 1)
->where('image_language', $locale)
->orderBy('image_sort')
->get();
```
View:
```blade
@foreach ($images as $image)
<figure>
<img src="{{ asset('storage/' . $image->image_file) }}"
alt="{{ $image->image_title }}">
<figcaption>{{ $image->image_title }}</figcaption>
</figure>
@endforeach
```
#### ContentPhotoGallery
| Table | Translation FK Field | Translation Language Field |
|-------|---------------------|--------------------------|
| `content_photo_gallery` | `gallery_translation_parent_id` → `gallery_id` | `gallery_translation_language` |
| Translatable Column | Type |
|--------------------|------|
| `gallery_translation_title` | `varchar(255)` |
| `gallery_translation_descr` | `text` (Summernote) |
```php
use App\Models\Backend\ContentPhotoGallery;
$locale = app()->getLocale();
$galleries = ContentPhotoGallery::with(['translations' => function ($q) use ($locale) {
$q->where('gallery_translation_language', $locale);
}])->orderBy('gallery_sort')->get();
```
View:
```blade
@foreach ($galleries as $gallery)
@php $trans = $gallery->translations->first(); @endphp
<h2>{{ $trans->gallery_translation_title ?? '-' }}</h2>
@if ($gallery->gallery_thumbnail)
<img src="{{ asset('storage/' . $gallery->gallery_thumbnail) }}">
@endif
<div>{!! $trans->gallery_translation_descr ?? '' !!}</div>
{{-- Senarai photo dalam gallery ni --}}
@php
$photos = App\Models\Backend\ContentPhotoList::where('photo_main', 1)
->where('photo_language', $locale)
->where('photo_gallery_id', $gallery->gallery_id)
->orderBy('photo_sort')
->get();
@endphp
@foreach ($photos as $photo)
<img src="{{ asset('storage/' . $photo->photo_url) }}">
<p>{{ $photo->photo_descr }}</p>
@endforeach
@endforeach
```
#### ContentPhotoList
| Column Parent | Column Sibling |
|--------------|---------------|
| `photo_main = 1` | `photo_main = 0` |
| `photo_parent_id = null` | `photo_parent_id` → parent `photo_id` |
| `photo_language` = locale | `photo_language` = locale |
| Translatable Column | Type |
|--------------------|------|
| `photo_descr` | `text` |
```php
$photos = ContentPhotoList::where('photo_main', 1)
->where('photo_language', app()->getLocale())
->when($galleryId, fn($q) => $q->where('photo_gallery_id', $galleryId))
->orderBy('photo_sort')
->get();
```
View:
```blade
@foreach ($photos as $photo)
<img src="{{ asset('storage/' . $photo->photo_url) }}" alt="{{ $photo->photo_descr }}">
<p>{{ $photo->photo_descr }}</p>
@endforeach
```
---
## 4. Model Scopes
| Module | Scope Available | Description |
|--------|---------------|-------------|
| ContentDownload | `main()` | Hanya parent rows (`download_main = 1`) |
| ContentDownload | `forLocale($locale)` | Eager load translations ikut locale |
Guna:
```php
ContentDownload::main()->forLocale('en')->get();
// atau guna locale semasa
ContentDownload::main()->forLocale()->get();
```
> **Nota:** Module flat sibling lain (ContentImage, ContentPhotoGallery, ContentPhotoList) **tiada scope khusus**. Guna query terus:
> ```php
> Module::where('*_main', 1)->where('*_language', $locale)->...
> ```
---
## 5. Language Switcher Dropdown
```blade
@php $currentLocale = app()->getLocale(); @endphp
<select onchange="window.location='{{ url('lang') }}/'+this.value">
@foreach ($languages as $code => $descr)
<option value="{{ $code }}" {{ $currentLocale == $code ? 'selected' : '' }}>
{{ $descr }}
</option>
@endforeach
</select>
```
`$languages` datang dari Ref table:
```php
$languages = Ref::where('cat', 'LANGUAGE')->orderBy('sort')->pluck('descr', 'code');
```
---
## 6. Admin: Cara Masukkan Translation
### ContentArticle, ContentApplication, ContentSlider, ContentVideo (Separate Table + Inline Alpine Tabs)
- Buka create/edit → ada card **Translations**
- Pill buttons / Dropdown untuk pilih bahasa
- Isi field translatable (title, content/description, untuk Slider: image upload per language)
- Tandakan **Set as Main Translation** untuk satu bahasa
- Submit → controller akan delete semua translation lama dan re-insert
### ContentPhotoGallery (Separate Table + Inline Alpine Tabs)
- Buka create/edit → ada card **Translations** dengan pill tabs / dropdown
- Isi field translatable (title/description) untuk setiap bahasa
- Tandakan **Set as Main Translation** untuk satu bahasa
- Submit → controller akan delete semua translation lama dan re-insert
### ContentPhotoList (Flat Sibling + Inline Alpine Tabs)
- Buka create/edit → ada card **Translations** dengan pill tabs
- Isi description untuk setiap bahasa
- Tandakan **Set as Main Translation** untuk satu bahasa
- Submit → controller akan delete semua sibling lama dan re-insert
- Parent row akan reflect data dari tab yang ditanda main
### ContentDownload, ContentImage (Flat Sibling — Separate Add/Delete Page)
- **Langkah 1:** Create parent entry dulu dengan 1 bahasa
- **Langkah 2:** Pada edit page, ada **Translations table** — senarai sibling sedia ada
- **Langkah 3:** Klik **Add Translation** → page berasingan untuk tambah sibling
- **Langkah 4:** Isi title, upload file/image untuk translation tu
- **Langkah 5:** Setiap translation boleh ada file dan image berbeza
---
## 7. Fallback Logic
### Separate Table (Article, Application, Slider, Video)
Bila display, eager load translation ikut locale. Kalau tiada translation untuk locale tersebut, fallback ke main translation (`*_translation_main = 1`):
```php
$article = ContentArticle::with(['translations' => function ($q) use ($locale) {
$q->where('article_translation_language', $locale)
->orWhere('article_translation_main', 1); // + main sebagai fallback
}])->findOrFail($id);
$translation = $article->translations->firstWhere('article_translation_language', $locale)
?? $article->translations->firstWhere('article_translation_main', 1);
```
### Flat Sibling (Download, Image, PhotoGallery, PhotoList)
Parent row sudah dalam locale semasa — data terus dari parent. Tapi kalau nak fallback untuk field tertentu dari sibling:
```blade
@php
$sibling = $download->translations->first();
@endphp
{{-- Parent punya field --}}
{{ $download->download_title }}
{{-- Fallback ke sibling kalau parent takde --}}
{{ $sibling->download_file ?? $download->download_file }}
```
---
## 8. Quick Reference: Field Names by Module
### Separate Translation Table
| Module | Parent PK | Translation FK | Translation Table | Lang Field | Main Field |
|--------|-----------|---------------|-------------------|------------|------------|
| ContentArticle | `article_id` | `article_translation_parent_id` | `content_article_translations` | `article_translation_language` | `article_translation_main` |
| ContentApplication | `application_id` | `application_translation_parent_id` | `content_application_translations` | `application_translation_language` | `application_translation_main` |
| ContentSlider | `slider_id` | `slider_translation_parent_id` | `content_slider_translations` | `slider_translation_language` | `slider_translation_main` |
| ContentVideo | `video_id` | `video_translation_parent_id` | `content_video_translations` | `video_translation_language` | `video_translation_main` |
| ContentPhotoGallery | `gallery_id` | `gallery_translation_parent_id` | `content_photo_gallery_translations` | `gallery_translation_language` | `gallery_translation_main` |
### Flat Sibling
| Module | Table | Main Flag | Parent FK | Language Field |
|--------|-------|-----------|-----------|---------------|
| ContentDownload | `content_downloads` | `download_main` | `download_parent_id` | `download_language` |
| ContentImage | `content_images` | `image_main` | `image_parent_id` | `image_language` |
| ContentPhotoGallery | `content_photo_gallery_translations` | `gallery_translation_main` | `gallery_translation_parent_id` | `gallery_translation_language` |
| ContentPhotoList | `content_photo_list` | `photo_main` | `photo_parent_id` | `photo_language` |
---
## 9. Files Reference
| File | Function |
|------|----------|
| `app/Http/Middleware/SetLocale.php` | Set locale dari session |
| `app/Http/Controllers/PublicController.php` | Contoh public controller |
| `app/Models/backendUser/ContentDownload.php` | Model Download + scopes |
| `docs/MODULE-REFERENCE.md` | Dokumentasi penuh setiap modul |
| `routes/web.php` | Routes public & admin |