start-vibing-stacks 2.0.4 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Start Vibing Stacks — Terminal UI
3
3
  */
4
4
  import chalk from 'chalk';
5
- const VERSION = '2.0.4';
5
+ const VERSION = '2.1.1';
6
6
  const gradient = (text) => {
7
7
  const colors = [chalk.hex('#FF6B6B'), chalk.hex('#FF8E53'), chalk.hex('#FFBD2E'), chalk.hex('#48BB78'), chalk.hex('#4299E1'), chalk.hex('#9F7AEA')];
8
8
  return text.split('').map((c, i) => colors[i % colors.length](c)).join('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-vibing-stacks",
3
- "version": "2.0.4",
3
+ "version": "2.1.1",
4
4
  "description": "AI-powered multi-stack dev workflow for Claude Code. Supports PHP, Node.js, Python and more.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,513 @@
1
+ # External API Patterns — HTTP Client for Laravel + Octane
2
+
3
+ **ALWAYS invoke when consuming external APIs, webhooks, or third-party services.**
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ Controller/Job
9
+ → ApiService (business logic)
10
+ → Http::withOptions() (Laravel HTTP Client)
11
+ → Response handling (DTO)
12
+ → Error handling (typed exceptions)
13
+ → Logging + monitoring
14
+
15
+ NEVER call Http:: directly in controllers. Always through a Service.
16
+ ```
17
+
18
+ ## Service Pattern
19
+
20
+ ```php
21
+ // app/Services/External/OpenAiService.php
22
+ namespace App\Services\External;
23
+
24
+ use App\DTOs\Api\ChatCompletionRequest;
25
+ use App\DTOs\Api\ChatCompletionResponse;
26
+ use App\Exceptions\Api\ApiConnectionException;
27
+ use App\Exceptions\Api\ApiRateLimitException;
28
+ use App\Exceptions\Api\ApiValidationException;
29
+ use Illuminate\Http\Client\PendingRequest;
30
+ use Illuminate\Http\Client\RequestException;
31
+ use Illuminate\Http\Client\ConnectionException;
32
+ use Illuminate\Support\Facades\Http;
33
+ use Illuminate\Support\Facades\Log;
34
+
35
+ class OpenAiService
36
+ {
37
+ private PendingRequest $client;
38
+
39
+ public function __construct()
40
+ {
41
+ $this->client = Http::baseUrl(config('services.openai.base_url', 'https://api.openai.com/v1'))
42
+ ->withToken(config('services.openai.key'))
43
+ ->timeout(30)
44
+ ->connectTimeout(5)
45
+ ->withHeaders([
46
+ 'Accept' => 'application/json',
47
+ 'Content-Type' => 'application/json',
48
+ ])
49
+ ->retry(
50
+ times: 3,
51
+ sleepMilliseconds: fn (int $attempt) => $attempt * 500, // 500ms, 1s, 1.5s
52
+ when: fn ($exception) => $this->shouldRetry($exception),
53
+ throw: true,
54
+ );
55
+ }
56
+
57
+ public function chatCompletion(ChatCompletionRequest $request): ChatCompletionResponse
58
+ {
59
+ try {
60
+ $response = $this->client
61
+ ->post('/chat/completions', $request->toArray())
62
+ ->throw();
63
+
64
+ return ChatCompletionResponse::fromArray($response->json());
65
+
66
+ } catch (ConnectionException $e) {
67
+ Log::error('[OpenAI] Connection failed', [
68
+ 'error' => $e->getMessage(),
69
+ ]);
70
+ throw new ApiConnectionException('OpenAI', $e);
71
+
72
+ } catch (RequestException $e) {
73
+ $this->handleRequestException($e, 'chatCompletion');
74
+ }
75
+ }
76
+
77
+ private function shouldRetry(\Exception $exception): bool
78
+ {
79
+ if ($exception instanceof ConnectionException) return true;
80
+
81
+ if ($exception instanceof RequestException) {
82
+ $status = $exception->response->status();
83
+ // Retry on: 408 timeout, 429 rate limit, 500+ server errors
84
+ return in_array($status, [408, 429, 500, 502, 503, 504]);
85
+ }
86
+
87
+ return false;
88
+ }
89
+
90
+ private function handleRequestException(RequestException $e, string $method): never
91
+ {
92
+ $status = $e->response->status();
93
+ $body = $e->response->json();
94
+
95
+ Log::error("[OpenAI] {$method} failed", [
96
+ 'status' => $status,
97
+ 'error' => $body['error']['message'] ?? $e->getMessage(),
98
+ 'type' => $body['error']['type'] ?? 'unknown',
99
+ ]);
100
+
101
+ match (true) {
102
+ $status === 429 => throw new ApiRateLimitException('OpenAI', $body, $e),
103
+ $status === 422 => throw new ApiValidationException('OpenAI', $body, $e),
104
+ $status >= 500 => throw new ApiConnectionException('OpenAI', $e),
105
+ default => throw new ApiConnectionException('OpenAI', $e),
106
+ };
107
+ }
108
+ }
109
+ ```
110
+
111
+ ## DTOs (Data Transfer Objects)
112
+
113
+ ```php
114
+ // app/DTOs/Api/ChatCompletionRequest.php
115
+ namespace App\DTOs\Api;
116
+
117
+ readonly class ChatCompletionRequest
118
+ {
119
+ public function __construct(
120
+ public string $model,
121
+ public array $messages,
122
+ public float $temperature = 0.7,
123
+ public int $maxTokens = 4096,
124
+ ) {}
125
+
126
+ public function toArray(): array
127
+ {
128
+ return [
129
+ 'model' => $this->model,
130
+ 'messages' => $this->messages,
131
+ 'temperature' => $this->temperature,
132
+ 'max_tokens' => $this->maxTokens,
133
+ ];
134
+ }
135
+ }
136
+
137
+ // app/DTOs/Api/ChatCompletionResponse.php
138
+ namespace App\DTOs\Api;
139
+
140
+ readonly class ChatCompletionResponse
141
+ {
142
+ public function __construct(
143
+ public string $id,
144
+ public string $content,
145
+ public int $promptTokens,
146
+ public int $completionTokens,
147
+ public string $model,
148
+ public string $finishReason,
149
+ ) {}
150
+
151
+ public static function fromArray(array $data): self
152
+ {
153
+ return new self(
154
+ id: $data['id'],
155
+ content: $data['choices'][0]['message']['content'] ?? '',
156
+ promptTokens: $data['usage']['prompt_tokens'] ?? 0,
157
+ completionTokens: $data['usage']['completion_tokens'] ?? 0,
158
+ model: $data['model'],
159
+ finishReason: $data['choices'][0]['finish_reason'] ?? 'unknown',
160
+ );
161
+ }
162
+ }
163
+ ```
164
+
165
+ ## Typed Exceptions
166
+
167
+ ```php
168
+ // app/Exceptions/Api/ApiConnectionException.php
169
+ namespace App\Exceptions\Api;
170
+
171
+ class ApiConnectionException extends \RuntimeException
172
+ {
173
+ public function __construct(
174
+ public readonly string $service,
175
+ ?\Throwable $previous = null,
176
+ ) {
177
+ parent::__construct("Connection to {$service} API failed", 503, $previous);
178
+ }
179
+ }
180
+
181
+ // app/Exceptions/Api/ApiRateLimitException.php
182
+ class ApiRateLimitException extends \RuntimeException
183
+ {
184
+ public function __construct(
185
+ public readonly string $service,
186
+ public readonly array $body = [],
187
+ ?\Throwable $previous = null,
188
+ ) {
189
+ $retryAfter = $body['error']['retry_after'] ?? 'unknown';
190
+ parent::__construct("{$service} rate limit exceeded. Retry after: {$retryAfter}s", 429, $previous);
191
+ }
192
+ }
193
+
194
+ // app/Exceptions/Api/ApiValidationException.php
195
+ class ApiValidationException extends \RuntimeException
196
+ {
197
+ public function __construct(
198
+ public readonly string $service,
199
+ public readonly array $body = [],
200
+ ?\Throwable $previous = null,
201
+ ) {
202
+ $message = $body['error']['message'] ?? 'Validation failed';
203
+ parent::__construct("{$service}: {$message}", 422, $previous);
204
+ }
205
+ }
206
+ ```
207
+
208
+ ## API Response Standard (Your API → Frontend)
209
+
210
+ ```php
211
+ // app/Traits/ApiResponse.php
212
+ namespace App\Traits;
213
+
214
+ use Illuminate\Http\JsonResponse;
215
+
216
+ trait ApiResponse
217
+ {
218
+ protected function success(mixed $data = null, string $message = 'OK', int $status = 200): JsonResponse
219
+ {
220
+ return response()->json([
221
+ 'success' => true,
222
+ 'message' => $message,
223
+ 'data' => $data,
224
+ ], $status);
225
+ }
226
+
227
+ protected function created(mixed $data = null, string $message = 'Created'): JsonResponse
228
+ {
229
+ return $this->success($data, $message, 201);
230
+ }
231
+
232
+ protected function error(string $message, int $status = 400, array $errors = []): JsonResponse
233
+ {
234
+ $response = [
235
+ 'success' => false,
236
+ 'message' => $message,
237
+ ];
238
+
239
+ if (!empty($errors)) {
240
+ $response['errors'] = $errors;
241
+ }
242
+
243
+ return response()->json($response, $status);
244
+ }
245
+
246
+ protected function notFound(string $resource = 'Resource'): JsonResponse
247
+ {
248
+ return $this->error("{$resource} not found", 404);
249
+ }
250
+
251
+ protected function unauthorized(string $message = 'Unauthorized'): JsonResponse
252
+ {
253
+ return $this->error($message, 401);
254
+ }
255
+
256
+ protected function rateLimited(int $retryAfter = 60): JsonResponse
257
+ {
258
+ return response()->json([
259
+ 'success' => false,
260
+ 'message' => 'Too many requests',
261
+ 'retry_after' => $retryAfter,
262
+ ], 429)->header('Retry-After', $retryAfter);
263
+ }
264
+ }
265
+ ```
266
+
267
+ ### Controller Usage
268
+
269
+ ```php
270
+ class AiModelController extends Controller
271
+ {
272
+ use ApiResponse;
273
+
274
+ public function __construct(
275
+ private readonly OpenAiService $openAi,
276
+ ) {}
277
+
278
+ public function generate(GenerateRequest $request): JsonResponse
279
+ {
280
+ try {
281
+ $dto = new ChatCompletionRequest(
282
+ model: $request->validated('model'),
283
+ messages: $request->validated('messages'),
284
+ temperature: $request->validated('temperature', 0.7),
285
+ );
286
+
287
+ $result = $this->openAi->chatCompletion($dto);
288
+
289
+ return $this->success([
290
+ 'content' => $result->content,
291
+ 'tokens' => $result->promptTokens + $result->completionTokens,
292
+ 'model' => $result->model,
293
+ ]);
294
+
295
+ } catch (ApiRateLimitException $e) {
296
+ return $this->rateLimited(60);
297
+ } catch (ApiValidationException $e) {
298
+ return $this->error($e->getMessage(), 422);
299
+ } catch (ApiConnectionException $e) {
300
+ return $this->error('Service temporarily unavailable', 503);
301
+ }
302
+ }
303
+ }
304
+ ```
305
+
306
+ ## Frontend Consumption (React + Inertia)
307
+
308
+ ```tsx
309
+ // resources/js/services/api.ts
310
+ import axios, { AxiosError } from 'axios';
311
+
312
+ interface ApiResponse<T> {
313
+ success: boolean;
314
+ message: string;
315
+ data: T;
316
+ errors?: Record<string, string[]>;
317
+ }
318
+
319
+ interface ApiErrorResponse {
320
+ success: false;
321
+ message: string;
322
+ errors?: Record<string, string[]>;
323
+ retry_after?: number;
324
+ }
325
+
326
+ export async function apiCall<T>(
327
+ method: 'get' | 'post' | 'put' | 'delete',
328
+ url: string,
329
+ data?: unknown,
330
+ ): Promise<T> {
331
+ try {
332
+ const response = await axios({ method, url, data });
333
+ const body = response.data as ApiResponse<T>;
334
+ if (!body.success) throw new ApiError(body.message, response.status);
335
+ return body.data;
336
+ } catch (error) {
337
+ if (error instanceof AxiosError) {
338
+ const body = error.response?.data as ApiErrorResponse;
339
+ const status = error.response?.status ?? 500;
340
+
341
+ if (status === 429) {
342
+ throw new RateLimitError(body?.retry_after ?? 60);
343
+ }
344
+ if (status === 422 && body?.errors) {
345
+ throw new ValidationError(body.message, body.errors);
346
+ }
347
+ throw new ApiError(body?.message ?? 'Connection failed', status);
348
+ }
349
+ throw error;
350
+ }
351
+ }
352
+
353
+ // Typed errors
354
+ export class ApiError extends Error {
355
+ constructor(message: string, public status: number) { super(message); }
356
+ }
357
+ export class ValidationError extends ApiError {
358
+ constructor(message: string, public errors: Record<string, string[]>) { super(message, 422); }
359
+ }
360
+ export class RateLimitError extends ApiError {
361
+ constructor(public retryAfter: number) { super('Too many requests', 429); }
362
+ }
363
+ ```
364
+
365
+ ### React Component Usage
366
+
367
+ ```tsx
368
+ const LABELS = {
369
+ generating: __('messages.ai.generating'),
370
+ error: __('messages.errors.error'),
371
+ rateLimited: __('messages.errors.rate_limited'),
372
+ } as const;
373
+
374
+ export default function AiChat() {
375
+ const [loading, setLoading] = useState(false);
376
+
377
+ const generate = async () => {
378
+ setLoading(true);
379
+ try {
380
+ const result = await apiCall<{ content: string }>('post', route('api.v1.ai.generate'), {
381
+ model: 'gpt-4',
382
+ messages: [{ role: 'user', content: prompt }],
383
+ });
384
+ setResponse(result.content);
385
+ } catch (error) {
386
+ if (error instanceof RateLimitError) {
387
+ toast.error(`${LABELS.rateLimited} (${error.retryAfter}s)`);
388
+ } else if (error instanceof ValidationError) {
389
+ Object.values(error.errors).flat().forEach(msg => toast.error(msg));
390
+ } else {
391
+ toast.error(error instanceof ApiError ? error.message : LABELS.error);
392
+ }
393
+ } finally {
394
+ setLoading(false);
395
+ }
396
+ };
397
+ }
398
+ ```
399
+
400
+ ## Octane Safety
401
+
402
+ ```php
403
+ // ✅ New client instance per request (no stale state)
404
+ public function __construct()
405
+ {
406
+ // Http::baseUrl() creates a NEW PendingRequest each time
407
+ // Safe in Octane — no shared state between requests
408
+ $this->client = Http::baseUrl(config('services.openai.base_url'))
409
+ ->withToken(config('services.openai.key'));
410
+ }
411
+
412
+ // ❌ NEVER static client (leaks between Octane requests)
413
+ private static PendingRequest $client; // ❌ Shared across ALL requests!
414
+ ```
415
+
416
+ ## Config Pattern
417
+
418
+ ```php
419
+ // config/services.php
420
+ 'openai' => [
421
+ 'key' => env('OPENAI_API_KEY'),
422
+ 'base_url' => env('OPENAI_BASE_URL', 'https://api.openai.com/v1'),
423
+ 'timeout' => env('OPENAI_TIMEOUT', 30),
424
+ 'max_retries' => env('OPENAI_MAX_RETRIES', 3),
425
+ ],
426
+
427
+ 'stripe' => [
428
+ 'key' => env('STRIPE_KEY'),
429
+ 'secret' => env('STRIPE_SECRET'),
430
+ 'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
431
+ ],
432
+ ```
433
+
434
+ **Rule:** All API keys in `.env` → `config/services.php` → `config('services.x.key')`. NEVER `env()` directly in code (cached in Octane).
435
+
436
+ ## Webhook Receiving
437
+
438
+ ```php
439
+ // app/Http/Controllers/Webhooks/StripeWebhookController.php
440
+ class StripeWebhookController extends Controller
441
+ {
442
+ public function handle(Request $request): JsonResponse
443
+ {
444
+ // 1. Verify signature
445
+ $payload = $request->getContent();
446
+ $signature = $request->header('Stripe-Signature');
447
+
448
+ try {
449
+ $event = \Stripe\Webhook::constructEvent(
450
+ $payload,
451
+ $signature,
452
+ config('services.stripe.webhook_secret')
453
+ );
454
+ } catch (\Exception $e) {
455
+ Log::warning('[Stripe Webhook] Invalid signature', ['error' => $e->getMessage()]);
456
+ return response()->json(['error' => 'Invalid signature'], 400);
457
+ }
458
+
459
+ // 2. Process idempotently (check if already processed)
460
+ if (WebhookEvent::where('event_id', $event->id)->exists()) {
461
+ return response()->json(['status' => 'already_processed']);
462
+ }
463
+
464
+ // 3. Store event
465
+ WebhookEvent::create([
466
+ 'event_id' => $event->id,
467
+ 'type' => $event->type,
468
+ 'payload' => $payload,
469
+ ]);
470
+
471
+ // 4. Dispatch job (async processing)
472
+ ProcessStripeEvent::dispatch($event->type, $event->data->object);
473
+
474
+ return response()->json(['status' => 'received']);
475
+ }
476
+ }
477
+ ```
478
+
479
+ ## Logging Standard
480
+
481
+ ```php
482
+ // Structured logging for all API calls
483
+ Log::info('[ServiceName] API call', [
484
+ 'method' => 'POST',
485
+ 'endpoint' => '/chat/completions',
486
+ 'status' => 200,
487
+ 'duration_ms' => $duration,
488
+ 'tokens' => $response->promptTokens + $response->completionTokens,
489
+ ]);
490
+
491
+ Log::error('[ServiceName] API failed', [
492
+ 'method' => 'POST',
493
+ 'endpoint' => '/chat/completions',
494
+ 'status' => $e->response->status(),
495
+ 'error' => $e->response->json('error.message'),
496
+ 'duration_ms' => $duration,
497
+ ]);
498
+ ```
499
+
500
+ ## FORBIDDEN
501
+
502
+ | ❌ Don't | ✅ Do |
503
+ |---|---|
504
+ | `Http::get()` in controller | Service class with typed DTOs |
505
+ | `env('API_KEY')` in code | `config('services.x.key')` |
506
+ | Raw arrays for API data | `readonly class` DTOs |
507
+ | Catch generic `Exception` | Typed exceptions per error type |
508
+ | `static $client` in Octane | Instance `$this->client` per request |
509
+ | No timeout on HTTP calls | `->timeout(30)->connectTimeout(5)` |
510
+ | No retry logic | `->retry(3, backoff, when)` |
511
+ | Webhook without signature check | ALWAYS verify signatures |
512
+ | Webhook sync processing | Dispatch job for async |
513
+ | `dd()` / `dump()` API responses | Structured `Log::info/error` |
@@ -0,0 +1,294 @@
1
+ # Laravel + Inertia i18n — Centralized Translations
2
+
3
+ **ALWAYS invoke when adding translations, creating new pages, or working with multilingual content.**
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ Laravel Backend Inertia Share React Frontend
9
+ ┌─────────────────┐ ┌──────────────┐ ┌──────────────────┐
10
+ │ lang/en/*.php │──→ config ──→│ InertiaShare │──→ props →│ __('key') │
11
+ │ lang/pt/*.php │ mapping │ translations │ │ via usePage() │
12
+ │ lang/en.json │ │ + cache │ │ │
13
+ └─────────────────┘ └──────────────┘ └──────────────────┘
14
+
15
+ Key decisions:
16
+ - Translations live in Laravel (single source of truth)
17
+ - Only NEEDED translations sent per page (not all)
18
+ - Cached forever (invalidate on deploy)
19
+ - Frontend reads via usePage().props.translations
20
+ ```
21
+
22
+ ## Backend: Translation Files
23
+
24
+ ### File Structure
25
+
26
+ ```
27
+ lang/
28
+ ├── en/
29
+ │ ├── messages.php # Global (loaded on all pages)
30
+ │ ├── validation.php # Laravel validation messages
31
+ │ ├── auth.php # Auth pages
32
+ │ └── admin/
33
+ │ ├── dashboard.php # Admin dashboard specific
34
+ │ └── ai-models.php # AI models page specific
35
+ ├── pt/
36
+ │ ├── messages.php
37
+ │ ├── validation.php
38
+ │ └── admin/
39
+ │ ├── dashboard.php
40
+ │ └── ai-models.php
41
+ ├── en.json # JSON format (optional, merged)
42
+ └── pt.json
43
+ ```
44
+
45
+ ### Translation File Example
46
+
47
+ ```php
48
+ // lang/en/messages.php
49
+ return [
50
+ 'common' => [
51
+ 'buttons' => [
52
+ 'save' => 'Save',
53
+ 'cancel' => 'Cancel',
54
+ 'close' => 'Close',
55
+ 'saving' => 'Saving...',
56
+ 'add_ai_model' => 'Add AI Model',
57
+ ],
58
+ ],
59
+ 'errors' => [
60
+ 'error' => 'Error',
61
+ 'api_save_failed_generic' => 'Failed to save. Please try again.',
62
+ 'validation_failed_check_fields' => 'Validation failed. Please check the fields.',
63
+ 'invalid_json' => 'Invalid JSON format.',
64
+ ],
65
+ 'admin' => [
66
+ 'ai' => [
67
+ 'ai-models' => [
68
+ 'new_model_button' => 'New AI Model',
69
+ 'add_modal_description' => 'Fill in the details to add a new AI model.',
70
+ 'fields' => [
71
+ 'name' => 'Name',
72
+ 'slug' => 'Slug',
73
+ 'system_prompt' => 'System Prompt',
74
+ 'temperature' => 'Temperature',
75
+ // ...
76
+ ],
77
+ ],
78
+ ],
79
+ ],
80
+ ];
81
+ ```
82
+
83
+ ## Backend: Page-to-Translation Mapping
84
+
85
+ ### Config File
86
+
87
+ ```php
88
+ // config/translations_inertia.php
89
+ return [
90
+ // Global: loaded on EVERY page
91
+ 'global' => [
92
+ 'messages', // lang/{locale}/messages.php
93
+ ],
94
+
95
+ // Per-page: loaded only when visiting that route
96
+ 'pages' => [
97
+ 'admin/dashboard' => ['admin/dashboard'],
98
+ 'admin/ai-models' => ['admin/ai-models'],
99
+ 'auth/login' => ['auth'],
100
+ 'auth/register' => ['auth'],
101
+ ],
102
+ ];
103
+ ```
104
+
105
+ **Rule:** Only send translations the page actually needs — not the entire `lang/` folder.
106
+
107
+ ## Backend: InertiaShare Middleware
108
+
109
+ ```php
110
+ // app/Support/InertiaShare.php
111
+ class InertiaShare
112
+ {
113
+ public static function getProps(Request $request): array
114
+ {
115
+ $locale = App::getLocale();
116
+
117
+ return [
118
+ 'auth' => [
119
+ 'user' => $request->user()?->only('id', 'name', 'email', 'role', 'timezone', 'avatar_url'),
120
+ ],
121
+ 'locale' => $locale,
122
+ 'translations' => static::getTranslations($locale, $request->route()->uri),
123
+ 'flash' => [
124
+ 'success' => session('success'),
125
+ 'error' => session('error'),
126
+ ],
127
+ ];
128
+ }
129
+
130
+ protected static function getTranslations(string $locale, string $pageUri): array
131
+ {
132
+ $cacheKey = "translations_{$locale}_{$pageUri}";
133
+
134
+ return Cache::rememberForever($cacheKey, function () use ($locale, $pageUri) {
135
+ $globalFiles = config('translations_inertia.global', []);
136
+ $pageFiles = config("translations_inertia.pages.{$pageUri}", []);
137
+ $files = array_unique(array_merge($globalFiles, $pageFiles));
138
+
139
+ $translations = [];
140
+ $langPath = lang_path($locale);
141
+
142
+ foreach ($files as $file) {
143
+ $filePath = "{$langPath}/{$file}.php";
144
+ if (File::exists($filePath)) {
145
+ $translations[$file] = require $filePath;
146
+ }
147
+ }
148
+
149
+ // Merge JSON translations (lang/{locale}.json)
150
+ $jsonPath = lang_path("{$locale}.json");
151
+ if (File::exists($jsonPath)) {
152
+ $json = json_decode(File::get($jsonPath), true);
153
+ if (is_array($json)) {
154
+ $translations = array_merge($translations, $json);
155
+ }
156
+ }
157
+
158
+ return $translations;
159
+ });
160
+ }
161
+ }
162
+ ```
163
+
164
+ ### Register in Middleware
165
+
166
+ ```php
167
+ // app/Http/Middleware/HandleInertiaRequests.php
168
+ use App\Support\InertiaShare;
169
+
170
+ public function share(Request $request): array
171
+ {
172
+ return array_merge(parent::share($request), InertiaShare::getProps($request));
173
+ }
174
+ ```
175
+
176
+ ## Frontend: translate.js Utility
177
+
178
+ ```js
179
+ // resources/js/Utils/translate.js
180
+ import { usePage } from '@inertiajs/react';
181
+
182
+ /**
183
+ * Get translated string by dot-notation key.
184
+ * @param {string} key - e.g. 'messages.common.buttons.save'
185
+ * @param {object} replacements - e.g. { name: 'John' } for ':name'
186
+ * @returns {string} Translated string or key as fallback
187
+ */
188
+ export default function __(key, replacements = {}) {
189
+ const { translations } = usePage().props;
190
+
191
+ let translation = key.split('.').reduce(
192
+ (obj, part) => (obj && obj[part] !== undefined ? obj[part] : null),
193
+ translations
194
+ );
195
+
196
+ // Fallback: return key if not found
197
+ if (translation === null || translation === undefined) {
198
+ return key;
199
+ }
200
+
201
+ // Replace :placeholders
202
+ if (typeof translation === 'string' && Object.keys(replacements).length > 0) {
203
+ Object.keys(replacements).forEach((placeholder) => {
204
+ translation = translation.replace(
205
+ new RegExp(`:${placeholder}`, 'g'),
206
+ replacements[placeholder]
207
+ );
208
+ });
209
+ }
210
+
211
+ return translation;
212
+ }
213
+ ```
214
+
215
+ ## Frontend: Component Pattern (CONST)
216
+
217
+ ```tsx
218
+ import __ from '@/Utils/translate';
219
+
220
+ // ═══════════════════════════════════════════
221
+ // 1. TRANSLATIONS — before hooks, outside component
222
+ // ═══════════════════════════════════════════
223
+ const LABELS = {
224
+ title: __('messages.admin.ai.ai-models.new_model_button'),
225
+ description: __('messages.admin.ai.ai-models.add_modal_description'),
226
+ nameField: __('messages.admin.ai.ai-models.fields.name'),
227
+ slugField: __('messages.admin.ai.ai-models.fields.slug'),
228
+ save: __('messages.common.buttons.save'),
229
+ saving: __('messages.common.buttons.saving'),
230
+ cancel: __('messages.common.buttons.cancel'),
231
+ errorTitle: __('messages.errors.error'),
232
+ validationFailed: __('messages.errors.validation_failed_check_fields'),
233
+ } as const;
234
+
235
+ // ═══════════════════════════════════════════
236
+ // 2. CSS STYLES — semantic tokens
237
+ // ═══════════════════════════════════════════
238
+ const STYLES = {
239
+ modal: 'relative w-full max-w-[558px] rounded-3xl bg-card p-6 lg:p-10',
240
+ title: 'mb-6 text-xl font-semibold text-foreground',
241
+ description: 'mb-7 text-sm leading-6 text-muted-foreground',
242
+ form: 'space-y-4',
243
+ gridTwo: 'grid grid-cols-2 gap-4',
244
+ actions: 'mt-8 flex w-full flex-col sm:flex-row items-center justify-between gap-3',
245
+ fieldError: 'text-sm text-destructive mt-1',
246
+ btnFull: 'w-full',
247
+ } as const;
248
+
249
+ // ═══════════════════════════════════════════
250
+ // 3. COMPONENT
251
+ // ═══════════════════════════════════════════
252
+ export default function AiModelsAddModal({ isOpen, onClose, onAdded }) {
253
+ // hooks, state, handlers...
254
+
255
+ return (
256
+ <Modal isOpen={isOpen} onClose={onClose} className={STYLES.modal}>
257
+ <h4 className={STYLES.title}>{LABELS.title}</h4>
258
+ <p className={STYLES.description}>{LABELS.description}</p>
259
+ {/* form fields using LABELS and STYLES */}
260
+ </Modal>
261
+ );
262
+ }
263
+ ```
264
+
265
+ ## Adding New Translations
266
+
267
+ ### Checklist
268
+
269
+ 1. Add string to `lang/en/{file}.php` **and** `lang/pt/{file}.php`
270
+ 2. If new file: add to `config/translations_inertia.php` (global or page-specific)
271
+ 3. Clear cache: `php artisan cache:clear`
272
+ 4. In React component: add to `LABELS` const using `__('key')`
273
+ 5. **NEVER** call `__()` inside JSX directly — always via LABELS const
274
+
275
+ ### Cache Invalidation
276
+
277
+ ```bash
278
+ # After adding/changing translations:
279
+ php artisan cache:clear
280
+
281
+ # For Octane (in-memory):
282
+ php artisan octane:reload
283
+ ```
284
+
285
+ ## FORBIDDEN
286
+
287
+ | ❌ Don't | ✅ Do |
288
+ |---|---|
289
+ | `__()` inside JSX return | `LABELS.title` via const |
290
+ | Send ALL translations to every page | Map per-page in config |
291
+ | Hardcode strings in components | Use translation keys |
292
+ | Skip Portuguese translation | Always add both `en` + `pt` |
293
+ | `Cache::forget()` for translations | `cache:clear` on deploy |
294
+ | Translation keys in camelCase | Use dot.notation `messages.admin.title` |