# Text Label System — GMP Platform

Panduan penerapan teks terpusat (label, button, notifikasi, validasi) untuk semua aplikasi GMP Platform.

---

## Daftar Isi

- [Arsitektur](#arsitektur)
- [Frontend](#frontend)
  - [Cara kerja](#cara-kerja)
  - [useAppText — hook utama](#useapptext--hook-utama)
  - [Interpolasi variabel](#interpolasi-variabel)
  - [i18n.language](#i18nlanguage)
  - [Fallback key](#fallback-key)
  - [Menambahkan app baru](#menambahkan-app-baru-frontend)
  - [Extend atau override teks common](#extend-atau-override-teks-common)
  - [Referensi key — common](#referensi-key--common)
- [Backend](#backend)
  - [Cara kerja](#cara-kerja-1)
  - [Penggunaan di controller](#penggunaan-di-controller)
  - [Penggunaan di Form Request](#penggunaan-di-form-request)
  - [Menambahkan app baru](#menambahkan-app-baru-backend)
  - [Referensi key — backend](#referensi-key--backend)
- [Konvensi penamaan key](#konvensi-penamaan-key)
- [Aturan umum](#aturan-umum)

---

## Arsitektur

```
packages/
  utils/
    common-resources.ts   ← teks umum shared semua app (frontend)
    text-utils.ts         ← helper: interpolate(), resolveKeyPath()
    index.ts              ← re-export semua dari package

  laravel-core/
    lang/id/
      common.php          ← label, button, status (backend)
      messages.php        ← notifikasi sukses/error (backend)
      validation.php      ← pesan validasi form (backend)
    src/
      LangServiceProvider.php  ← register lang dengan namespace "gmp"

apps/{app}/
  resources/js/lib/
    text-resources.ts     ← teks domain-specific app + spread commonResources
    app-text.ts           ← hook useAppText() siap pakai
  lang/id/
    {module}.php          ← teks domain-specific per modul (opsional)
```

Prinsip dasarnya:

- **`packages/`** → sumber kebenaran tunggal untuk teks yang **sama di semua app**
- **`apps/{app}/`** → hanya berisi teks yang **unik untuk domain tersebut**

---

## Frontend

### Cara kerja

```
components
    ↓  import useAppText()
app-text.ts  (per app)
    ↓  import resources
text-resources.ts  (per app)
    ↓  import commonResources
packages/utils/common-resources.ts  (shared)
```

`useAppText()` mengembalikan fungsi `t(key, options?)` yang mencari teks dari `resources.id` menggunakan dot notation, lalu melakukan interpolasi variabel jika diperlukan.

---

### `useAppText` — hook utama

```tsx
import { useAppText } from '@/lib/app-text';

export function MyComponent() {
    const { t } = useAppText();

    return (
        <div>
            <h1>{t('bank.title')}</h1>
            <button>{t('common.btn_create')}</button>
        </div>
    );
}
```

Key menggunakan **dot notation** yang mencerminkan struktur nested object di `text-resources.ts`.

```
"common.btn_delete"          →  resources.id.common.btn_delete
"bank.create_title"          →  resources.id.bank.create_title
"fund_request.field_amount"  →  resources.id.fund_request.field_amount
```

---

### Interpolasi variabel

Gunakan `{{ namaVariabel }}` di dalam string teks, lalu passing nilai sebagai object di parameter kedua.

```tsx
const { t } = useAppText();

// Template di text-resources.ts:
// msg_success_create: '{{module}} berhasil dibuat'
t('common.msg_success_create', { module: 'Invoice' });
// → "Invoice berhasil dibuat"

// Template:
// required: '{{field}} wajib diisi'
t('validation.required', { field: 'Nama Bank' });
// → "Nama Bank wajib diisi"

// Template:
// helper_upload_formats: 'Format yang diizinkan: {{formats}}'
t('common.helper_upload_formats', { formats: 'PDF, JPG, PNG' });
// → "Format yang diizinkan: PDF, JPG, PNG"
```

> Variabel yang tidak ditemukan dalam options akan diganti dengan string kosong `""`.

---

### `i18n.language`

Berguna untuk format tanggal/waktu menggunakan locale Indonesia.

```tsx
const { t, i18n } = useAppText();

// Gunakan untuk toLocaleTimeString, toLocaleDateString, dst.
date.toLocaleTimeString(i18n.language, { hour: '2-digit', minute: '2-digit' });
// → format 24 jam Indonesia, misal: "14:30"

date.toLocaleDateString(i18n.language, { day: 'numeric', month: 'long', year: 'numeric' });
// → "11 April 2026"
```

---

### Fallback key

Jika key tidak ditemukan di `resources.id`, fungsi `t()` mengembalikan **key itu sendiri** — bukan string kosong atau error.

```tsx
t('modul.key_yang_belum_ada');
// → "modul.key_yang_belum_ada"  (langsung terlihat di UI, mudah dideteksi)
```

---

### Menambahkan app baru (Frontend)

Buat dua file di `apps/{nama-app}/resources/js/lib/`:

**Langkah 1 — `text-resources.ts`**

```ts
import { commonResources } from '@gmp/utils';

interface TranslationTree {
    [key: string]: string | TranslationTree;
}

export const resources: { id: TranslationTree } = {
    id: {
        ...commonResources.id,   // spread semua teks common (wajib)
        // Tambahkan teks domain-specific di bawah ini:
        employee: {
            title: 'Karyawan',
            description: 'Kelola data karyawan',
            create_title: 'Tambah Karyawan',
            edit_title: 'Ubah Karyawan',
            field_name: 'Nama Karyawan',
            field_nik: 'NIK',
        },
    },
};
```

**Langkah 2 — `app-text.ts`** (salin persis, tidak ada yang perlu diubah)

```ts
import { interpolate, resolveKeyPath } from '@gmp/utils';
import { resources } from './text-resources';

type TOptions = Record<string, unknown>;

const i18n = { language: 'id' as const };

export function useAppText() {
    const t = (key: string, options?: TOptions): string => {
        const template = resolveKeyPath(resources.id, key) ?? key;
        return interpolate(template, options);
    };

    return { t, i18n };
}
```

---

### Extend atau override teks common

Jika sebuah app perlu menambahkan key ke section `common`, atau mengubah teks default, gunakan spread dan override:

```ts
export const resources = {
    id: {
        ...commonResources.id,
        // Extend section common dengan key baru
        common: {
            ...commonResources.id.common,
            menu_finance: 'Keuangan',         // tambah key baru
            btn_create: 'Buat Baru',          // override teks default
        },
    },
};
```

> Jangan hapus `...commonResources.id.common` saat extend — semua key yang tidak di-override tetap diambil dari common.

---

### Referensi key — common

#### `common.*`

| Key | Teks |
|---|---|
| `label_search` | Cari |
| `label_actions` | Aksi |
| `label_active_status` | Status aktif |
| `label_created_at` | Dibuat pada |
| `label_filter` | Pilih opsi |
| `label_child` | Anak |
| `btn_create` | Tambah |
| `btn_edit` | Ubah |
| `btn_delete` | Hapus |
| `btn_import` | Impor |
| `btn_export` | Ekspor |
| `btn_cancel` | Batal |
| `btn_reset` | Reset |
| `btn_previous` | Sebelumnya |
| `btn_next` | Berikutnya |
| `msg_error` | Terjadi kesalahan |
| `msg_loading` | Memuat... |
| `msg_no_data` | Tidak ada data |
| `msg_success_create` | `{{module}}` berhasil dibuat |
| `msg_success_update` | `{{module}}` berhasil diperbarui |
| `msg_success_delete` | `{{module}}` berhasil dihapus |
| `status_active` | Aktif |
| `status_inactive` | Nonaktif |
| `status_waiting` | Menunggu |
| `status_approved` | Disetujui |
| `status_rejected` | Ditolak |
| `status_on_progress` | Diproses |
| `status_done` | Selesai |
| `status_saving` | Menyimpan... |
| `just_now` | baru saja |
| `helper_upload_formats` | Format yang diizinkan: `{{formats}}` |

#### `dialog.*`

| Key | Teks |
|---|---|
| `delete_title` | Hapus `{{module}}` |

#### `drawer.*`

| Key | Teks |
|---|---|
| `create_description` | Buat `{{module}}` baru |
| `edit_description` | Ubah `{{module}}` |

#### `form.*`

| Key | Teks |
|---|---|
| `btn_submit_create` | Simpan `{{module}}` |
| `btn_submit_update` | Perbarui `{{module}}` |
| `placeholder_description` | Masukkan deskripsi |
| `label_is_active` | Status aktif |
| `desc_is_active` | Aktifkan data ini agar dapat digunakan di sistem |

#### `validation.*`

| Key | Teks |
|---|---|
| `required` | `{{field}}` wajib diisi |

---

## Backend

### Cara kerja

File lang disimpan di `packages/laravel-core/lang/id/` dan di-load otomatis oleh `LangServiceProvider` via Laravel package auto-discovery.

Untuk menyederhanakan pemanggilan, setiap app memiliki helper `text()` di `app/helpers.php` yang menangani lookup dua tahap secara otomatis:

```
text('bank.title')
    ↓ coba app-specific: lang/id/bank.php → "Bank" ✓  (selesai)

text('messages.success_create', ['module' => 'X'])
    ↓ coba app-specific: lang/id/messages.php → tidak ada
    ↓ fallback ke core: gmp::messages.success_create → "X berhasil dibuat" ✓
```

Kamu **tidak perlu tahu** apakah teks ada di core atau di app — cukup panggil `text()` dengan format `"group.key"` yang sama.

---

### Penggunaan di controller

Gunakan helper `text()` — **tidak perlu tahu** teks ada di core atau di app:

```php
use Illuminate\Http\JsonResponse;

class BankController extends Controller
{
    public function store(StoreBankRequest $request): JsonResponse
    {
        $bank = Bank::create($request->validated());

        return response()->json([
            'message' => text('messages.success_create', ['module' => 'Bank']),
        ]);
        // → "Bank berhasil dibuat"  (dari gmp::messages)
    }

    public function destroy(Bank $bank): JsonResponse
    {
        $bank->delete();

        return response()->json([
            'message' => text('messages.success_delete', ['module' => 'Bank']),
        ]);
    }

    public function notFound(): JsonResponse
    {
        return response()->json([
            'message' => text('messages.error_not_found', ['model' => 'Bank']),
        ], 404);
    }
}
```

`text()` mencoba app-specific terlebih dahulu, lalu otomatis fallback ke `gmp::` jika tidak ditemukan — **satu cara panggil untuk semua teks**.

> `__()` tetap bisa dipakai jika kamu sudah tahu persis namespace-nya. `text()` adalah shortcut saat tidak ingin memikirkan asal teksnya.

---

### Penggunaan di Form Request

```php
class StoreBankRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:100', 'unique:banks,name'],
            'code' => ['required', 'string', 'max:10'],
        ];
    }

    public function messages(): array
    {
        return [
            'name.required' => text('validation.required', ['field' => 'Nama Bank']),
            'name.unique'   => text('validation.unique',   ['field' => 'Nama Bank']),
            'name.max'      => text('validation.max_string', ['field' => 'Nama Bank', 'max' => 100]),
            'code.required' => text('validation.required', ['field' => 'Kode Bank']),
        ];
    }
}
```

---

### Menambahkan app baru (Backend)

Tidak perlu setup apapun — `LangServiceProvider` sudah ter-register otomatis di semua app yang menggunakan `gmp/laravel-core`. Langsung gunakan `__('gmp::...')`.

Untuk teks domain-specific, buat file lang di dalam app sendiri **tanpa namespace**:

```
apps/finance/lang/id/
  bank.php
  budget.php
  fund_request.php
```

```php
// apps/finance/lang/id/bank.php
return [
    'title'        => 'Bank',
    'create_title' => 'Tambah Bank',
    'field_name'   => 'Nama Bank',
    'field_code'   => 'Kode Bank',
];

// Penggunaan (tanpa namespace gmp::):
__('bank.title')          // → "Bank"
__('bank.create_title')   // → "Tambah Bank"
```

---

### Referensi key — backend

#### `gmp::common.*`

| Key | Teks |
|---|---|
| `label_search` | Cari |
| `label_actions` | Aksi |
| `label_active_status` | Status aktif |
| `label_created_at` | Dibuat pada |
| `label_filter` | Pilih opsi |
| `label_child` | Anak |
| `btn_save` | Simpan |
| `btn_create` | Tambah |
| `btn_edit` | Ubah |
| `btn_delete` | Hapus |
| `btn_import` | Impor |
| `btn_export` | Ekspor |
| `btn_cancel` | Batal |
| `btn_reset` | Reset |
| `btn_previous` | Sebelumnya |
| `btn_next` | Berikutnya |
| `btn_submit` | Kirim |
| `btn_confirm` | Konfirmasi |
| `btn_back` | Kembali |
| `btn_close` | Tutup |
| `status_active` | Aktif |
| `status_inactive` | Nonaktif |
| `status_waiting` | Menunggu |
| `status_approved` | Disetujui |
| `status_rejected` | Ditolak |
| `status_on_progress` | Diproses |
| `status_done` | Selesai |
| `just_now` | baru saja |
| `helper_upload_formats` | Format yang diizinkan: `:formats` |

#### `gmp::messages.*`

| Key | Parameter | Teks |
|---|---|---|
| `success_create` | `module` | `:module berhasil dibuat` |
| `success_update` | `module` | `:module berhasil diperbarui` |
| `success_delete` | `module` | `:module berhasil dihapus` |
| `success_import` | — | Data berhasil diimpor |
| `success_export` | — | Data berhasil diekspor |
| `success_save` | — | Data berhasil disimpan |
| `success_send` | — | Data berhasil dikirim |
| `error_general` | — | Terjadi kesalahan, silakan coba lagi |
| `error_not_found` | `model` | `:model tidak ditemukan` |
| `error_unauthorized` | — | Anda tidak memiliki akses untuk melakukan tindakan ini |
| `error_forbidden` | — | Akses ditolak |
| `error_server` | — | Kesalahan server, silakan hubungi administrator |
| `confirm_delete` | `model` | `Apakah Anda yakin ingin menghapus :model ini?` |
| `loading` | — | Memuat... |
| `no_data` | — | Tidak ada data |
| `saving` | — | Menyimpan... |

#### `gmp::validation.*`

| Key | Parameter | Teks |
|---|---|---|
| `required` | `field` | `:field wajib diisi` |
| `required_if` | `field`, `other` | `:field wajib diisi jika :other diisi` |
| `string` | `field` | `:field harus berupa teks` |
| `integer` | `field` | `:field harus berupa angka` |
| `numeric` | `field` | `:field harus berupa angka` |
| `email` | `field` | `:field harus berupa alamat email yang valid` |
| `url` | `field` | `:field harus berupa URL yang valid` |
| `date` | `field` | `:field harus berupa tanggal yang valid` |
| `boolean` | `field` | `:field harus berupa nilai benar/salah` |
| `array` | `field` | `:field harus berupa daftar` |
| `min_string` | `field`, `min` | `:field minimal :min karakter` |
| `max_string` | `field`, `max` | `:field maksimal :max karakter` |
| `min_numeric` | `field`, `min` | `:field minimal bernilai :min` |
| `max_numeric` | `field`, `max` | `:field maksimal bernilai :max` |
| `min_file` | `field`, `min` | `Ukuran file :field minimal :min KB` |
| `max_file` | `field`, `max` | `Ukuran file :field maksimal :max KB` |
| `unique` | `field` | `:field sudah digunakan` |
| `exists` | `field` | `:field tidak ditemukan di sistem` |
| `mimes` | `field`, `values` | `:field harus berformat: :values` |
| `image` | `field` | `:field harus berupa gambar` |

---

## Konvensi penamaan key

| Prefix | Digunakan untuk | Contoh |
|---|---|---|
| `label_` | Label kolom tabel / field | `label_search`, `label_created_at` |
| `btn_` | Teks tombol aksi | `btn_create`, `btn_delete` |
| `msg_` | Pesan state / notifikasi toast | `msg_error`, `msg_success_create` |
| `status_` | Teks status badge | `status_active`, `status_approved` |
| `field_` | Label input form | `field_name`, `field_amount` |
| `placeholder_` | Placeholder input | `placeholder_name`, `placeholder_code` |
| `helper_` | Teks hint di bawah input | `helper_amount_idr` |
| `section_` | Judul section dalam form | `section_basic_info` |
| `desc_` | Deskripsi panjang / subtitle | `desc_is_active` |
| `title` | Judul halaman / modul | `bank.title` |
| `description` | Deskripsi singkat halaman | `bank.description` |
| `create_title` | Judul drawer/modal create | `bank.create_title` |
| `edit_title` | Judul drawer/modal edit | `bank.edit_title` |
| `option_` | Pilihan dropdown / select | `option_opex`, `option_capex` |
| `step_` | Langkah wizard / stepper | `step_approvals` |
| `detail_` | Label section di halaman detail | `detail_information` |

---

## Aturan umum

1. **Jangan hardcode teks UI** langsung di komponen atau controller. Selalu gunakan `t()` atau `text()`.
2. **Frontend** — gunakan `t('group.key')`, tidak perlu tahu teks berasal dari `commonResources` atau app-specific.
3. **Backend** — gunakan `text('group.key')`, tidak perlu tahu teks berasal dari `gmp::` core atau `lang/` app.
4. **Teks yang sama di semua app** → simpan di `packages/utils/common-resources.ts` (frontend) atau `packages/laravel-core/lang/id/` (backend).
5. **Teks domain-specific** → simpan di `apps/{app}/resources/js/lib/text-resources.ts` (frontend) atau `apps/{app}/lang/id/{modul}.php` (backend).
6. **Jangan duplikasi** — jika teks sudah ada di common, langsung pakai. Jangan tulis ulang di app.
7. **Ikuti konvensi prefix** saat menambahkan key baru agar mudah dibaca dan dikelompokkan.
8. **Backend pakai `:param`**, frontend pakai `{{ param }}` — jangan tertukar.
