diff --git a/.claude/skills/ai-sdk-development/SKILL.md b/.claude/skills/ai-sdk-development/SKILL.md new file mode 100644 index 00000000..d74e342e --- /dev/null +++ b/.claude/skills/ai-sdk-development/SKILL.md @@ -0,0 +1,413 @@ +--- +name: ai-sdk-development +description: Builds AI agents, generates text and chat responses, produces images, synthesizes audio, transcribes speech, generates vector embeddings, reranks documents, and manages files and vector stores using the Laravel AI SDK (laravel/ai). Supports structured output, streaming, tools, conversation memory, middleware, queueing, broadcasting, and provider failover. Use when building, editing, updating, debugging, or testing any AI functionality, including agents, LLMs, chatbots, text generation, image generation, audio, transcription, embeddings, RAG, similarity search, vector stores, prompting, structured output, or any AI provider (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter). +--- + +# Developing with the Laravel AI SDK + +The Laravel AI SDK (`laravel/ai`) is the official AI package for Laravel, providing a unified API for agents, images, audio, transcription, embeddings, reranking, vector stores, and file management across multiple AI providers. + +## Searching the Documentation + +This package is new. Always search the documentation before implementing any feature. Never guess at APIs — the documentation is the single source of truth. + +- Use broad, simple queries that match the documentation section headings below. +- Do not add package names to queries — package information is shared automatically. Use `test agent fake`, not `laravel ai test agent fake`. +- Run multiple queries at once — the most relevant results are returned first. + +### Documentation Sections + +Use these section headings as query terms for accurate results: + +- Introduction, Installation, Configuration, Provider Support +- Agents: Prompting, Conversation Context, Structured Output, Attachments, Streaming, Broadcasting, Queueing, Tools, Provider Tools, Middleware, Anonymous Agents, Agent Configuration +- Images +- Audio (TTS) +- Transcription (STT) +- Embeddings: Querying Embeddings, Caching Embeddings +- Reranking +- Files +- Vector Stores: Adding Files to Stores +- Failover +- Testing: Agents, Images, Audio, Transcriptions, Embeddings, Reranking, Files, Vector Stores +- Events + +## Decision Workflow + +Determine the right entry point before writing code: + +Text generation or chat? → Agent class with `Promptable` trait +Chat with conversation history? → Agent + `Conversational` interface (manual) or `RemembersConversations` trait (automatic) +Structured JSON output? → Agent + `HasStructuredOutput` interface +Image generation? → `Image::of()->generate()` +Audio synthesis? → `Audio::of()->generate()` +Transcription? → `Transcription::fromPath()->generate()` +Embeddings? → `Embeddings::for()->generate()` +Reranking? → `Reranking::of()->rerank()` +File storage? → `Document::fromPath()->put()` +Vector stores? → `Stores::create()` + +## Basic Usage Examples + +### Agents + +```php +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Promptable; + +class SalesCoach implements Agent +{ + use Promptable; + + public function instructions(): string + { + return 'You are a sales coach.'; + } +} + +// Prompting +$response = (new SalesCoach)->prompt('Analyze this transcript...'); +echo $response->text; + +// Streaming (returns SSE response from a route) +return (new SalesCoach)->stream('Analyze this transcript...'); + +// Queueing +(new SalesCoach)->queue('Analyze this transcript...') + ->then(fn ($response) => /* ... */); + +// Anonymous agents +use function Laravel\Ai\{agent}; + +$response = agent(instructions: 'You are a helpful assistant.')->prompt('Hello'); +``` + +### Conversation Context + +Manual conversation history via the `Conversational` interface: + +```php +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Contracts\Conversational; +use Laravel\Ai\Messages\Message; +use Laravel\Ai\Promptable; + +class SalesCoach implements Agent, Conversational +{ + use Promptable; + + public function __construct(public User $user) {} + + public function instructions(): string { return 'You are a sales coach.'; } + + public function messages(): iterable + { + return History::where('user_id', $this->user->id) + ->latest()->limit(50)->get()->reverse() + ->map(fn ($m) => new Message($m->role, $m->content)) + ->all(); + } +} +``` + +Automatic conversation persistence via the `RemembersConversations` trait: + +```php +use Laravel\Ai\Concerns\RemembersConversations; +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Contracts\Conversational; +use Laravel\Ai\Promptable; + +class SalesCoach implements Agent, Conversational +{ + use Promptable, RemembersConversations; + + public function instructions(): string { return 'You are a sales coach.'; } +} + +// Start a new conversation +$response = (new SalesCoach)->forUser($user)->prompt('Hello!'); +$conversationId = $response->conversationId; + +// Continue an existing conversation +$response = (new SalesCoach)->continue($conversationId, as: $user)->prompt('Tell me more.'); +``` + +### Structured Output + +```php +use Illuminate\Contracts\JsonSchema\JsonSchema; +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Contracts\HasStructuredOutput; +use Laravel\Ai\Promptable; + +class Reviewer implements Agent, HasStructuredOutput +{ + use Promptable; + + public function instructions(): string { return 'Review and score content.'; } + + public function schema(JsonSchema $schema): array + { + return [ + 'feedback' => $schema->string()->required(), + 'score' => $schema->integer()->min(1)->max(10)->required(), + ]; + } +} + +$response = (new Reviewer)->prompt('Review this...'); +echo $response['score']; // Access like an array +``` + +### Images + +```php +use Laravel\Ai\Image; + +$image = Image::of('A sunset over mountains') + ->landscape() + ->quality('high') + ->generate(); + +$path = $image->store(); // Store to default disk +``` + +### Audio + +```php +use Laravel\Ai\Audio; + +$audio = Audio::of('Hello from Laravel.') + ->female() + ->instructions('Speak warmly') + ->generate(); + +$path = $audio->store(); +``` + +### Transcription + +```php +use Laravel\Ai\Transcription; + +$transcript = Transcription::fromStorage('audio.mp3') + ->diarize() + ->generate(); + +echo (string) $transcript; +``` + +### Embeddings + +```php +use Laravel\Ai\Embeddings; +use Illuminate\Support\Str; + +$response = Embeddings::for(['Text one', 'Text two']) + ->dimensions(1536) + ->cache() + ->generate(); + +// Single string via Stringable +$embedding = Str::of('Napa Valley has great wine.')->toEmbeddings(); +``` + +### Reranking + +```php +use Laravel\Ai\Reranking; + +$response = Reranking::of(['Django is Python.', 'Laravel is PHP.', 'React is JS.']) + ->limit(5) + ->rerank('PHP frameworks'); + +$response->first()->document; // "Laravel is PHP." +``` + +### Files and Vector Stores + +```php +use Laravel\Ai\Files\Document; +use Laravel\Ai\Stores; + +// Store a file with the provider +$file = Document::fromPath('/path/to/doc.pdf')->put(); + +// Create a vector store and add files +$store = Stores::create('Knowledge Base'); +$store->add($file->id); +$store->add(Document::fromStorage('manual.pdf')); // Store + add in one step +``` + +## Agent Configuration + +### PHP Attributes + +```php +use Laravel\Ai\Attributes\{Provider, MaxSteps, MaxTokens, Temperature, Timeout}; + +#[Provider('anthropic')] +#[MaxSteps(10)] +#[MaxTokens(4096)] +#[Temperature(0.7)] +#[Timeout(120)] +class MyAgent implements Agent +{ + use Promptable; + // ... +} +``` + +The `#[UseCheapestModel]` and `#[UseSmartestModel]` attributes are also available for automatic model selection. + +### Tools + +Implement the `HasTools` interface and scaffold tools with `php artisan make:tool`: + +```php +use Laravel\Ai\Contracts\HasTools; + +class MyAgent implements Agent, HasTools +{ + use Promptable; + + public function tools(): iterable + { + return [new MyCustomTool]; + } +} +``` + +### Provider Tools + +```php +use Laravel\Ai\Providers\Tools\{WebSearch, WebFetch, FileSearch}; + +public function tools(): iterable +{ + return [ + (new WebSearch)->max(5)->allow(['laravel.com']), + new WebFetch, + new FileSearch(stores: ['store_id']), + ]; +} +``` + +### Conversation Memory + +```php +use Laravel\Ai\Concerns\RemembersConversations; +use Laravel\Ai\Contracts\Conversational; + +class ChatBot implements Agent, Conversational +{ + use Promptable, RemembersConversations; + // ... +} + +$response = (new ChatBot)->forUser($user)->prompt('Hello!'); +$response = (new ChatBot)->continue($conversationId, as: $user)->prompt('More...'); +``` + +### Failover + +```php +$response = (new MyAgent)->prompt('Hello', provider: ['openai', 'anthropic']); +``` + +## Testing and Faking + +Each capability supports `fake()` with assertions: + +```php +use App\Ai\Agents\SalesCoach; +use Laravel\Ai\{Image, Audio, Transcription, Embeddings, Reranking, Files, Stores}; + +// Agents +SalesCoach::fake(['Response 1', 'Response 2']); +SalesCoach::assertPrompted('query'); +SalesCoach::assertNotPrompted('query'); +SalesCoach::assertNeverPrompted(); +SalesCoach::fake()->preventStrayPrompts(); + +// Images +Image::fake(); +Image::assertGenerated(fn ($prompt) => $prompt->contains('sunset')); +Image::assertNothingGenerated(); + +// Audio +Audio::fake(); +Audio::assertGenerated(fn ($prompt) => $prompt->contains('Hello')); + +// Transcription +Transcription::fake(['Transcribed text.']); +Transcription::assertGenerated(fn ($prompt) => $prompt->isDiarized()); + +// Embeddings +Embeddings::fake(); +Embeddings::assertGenerated(fn ($prompt) => $prompt->contains('Laravel')); + +// Reranking +Reranking::fake(); +Reranking::assertReranked(fn ($prompt) => $prompt->contains('PHP')); + +// Files +Files::fake(); +Files::assertStored(fn ($file) => $file->mimeType() === 'text/plain'); + +// Stores +Stores::fake(); +Stores::assertCreated('Knowledge Base'); +$store = Stores::get('id'); +$store->assertAdded('file_id'); +``` + +## Key Patterns + +- Namespace: `Laravel\Ai\` +- Package: `composer require laravel/ai` +- Agent pattern: Implement the `Agent` interface and use the `Promptable` trait +- Optional interfaces: `HasTools`, `HasMiddleware`, `HasStructuredOutput`, `Conversational` +- Entry-point classes: `Image`, `Audio`, `Transcription`, `Embeddings`, `Reranking`, `Stores` +- Artisan commands: `php artisan make:agent`, `php artisan make:tool` +- Global helper: `agent()` for anonymous agents + +## Common Pitfalls + +### Wrong Namespace + +The namespace is `Laravel\Ai`, not `Illuminate\Ai` or `Laravel\AI`. + +```php +// Correct +use Laravel\Ai\Image; +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Promptable; + +// Wrong — these do not exist +use Illuminate\Ai\Image; +use Laravel\AI\Agent; +``` + +### Unsupported Provider Capability + +Calling a capability not supported by a provider throws a `LogicException`. Refer to the provider support table below. + +### Never Use Prism Directly + +Use agents and entry-point classes (`Image`, `Audio`, etc.) — not `Prism::text()` directly. The AI SDK wraps Prism internally. + +## Provider Support + +| Provider | Text | Image | Audio | STT | Embeddings | Reranking | Files | Stores | +| ---------- | ---- | ----- | ----- | --- | ---------- | --------- | ----- | ------ | +| OpenAI | Y | Y | Y | Y | Y | - | Y | Y | +| Anthropic | Y | - | - | - | - | - | Y | - | +| Gemini | Y | Y | - | - | Y | - | Y | Y | +| xAI | Y | Y | - | - | - | - | - | - | +| Groq | Y | - | - | - | - | - | - | - | +| OpenRouter | Y | - | - | - | - | - | - | - | +| ElevenLabs | - | - | Y | Y | - | - | - | - | +| Cohere | - | - | - | - | Y | Y | - | - | +| Jina | - | - | - | - | Y | Y | - | - | \ No newline at end of file diff --git a/.claude/skills/cashier-stripe-development/SKILL.md b/.claude/skills/cashier-stripe-development/SKILL.md index 3611b62e..355e8a92 100644 --- a/.claude/skills/cashier-stripe-development/SKILL.md +++ b/.claude/skills/cashier-stripe-development/SKILL.md @@ -33,9 +33,9 @@ ## Basic Usage ### Installation ```bash -vendor/bin/sail artisan vendor:publish --tag="cashier-migrations" -vendor/bin/sail artisan migrate -vendor/bin/sail artisan vendor:publish --tag="cashier-config" +php artisan vendor:publish --tag="cashier-migrations" +php artisan migrate +php artisan vendor:publish --tag="cashier-config" ``` ### Environment Variables diff --git a/.claude/skills/configuring-horizon/SKILL.md b/.claude/skills/configuring-horizon/SKILL.md index 112c0d13..bed1e74c 100644 --- a/.claude/skills/configuring-horizon/SKILL.md +++ b/.claude/skills/configuring-horizon/SKILL.md @@ -24,7 +24,7 @@ ## Basic Usage ### Installation ```bash -vendor/bin/sail artisan horizon:install +php artisan horizon:install ``` ### Supervisor Configuration @@ -70,7 +70,7 @@ ### Dashboard Authorization ## Verification -1. Run `vendor/bin/sail artisan horizon` and visit `/horizon` +1. Run `php artisan horizon` and visit `/horizon` 2. Confirm dashboard access is restricted as expected 3. Check that metrics populate after scheduling `horizon:snapshot` @@ -81,5 +81,5 @@ ## Common Pitfalls - Always check `config/horizon.php` before making changes to understand the current supervisor and environment configuration. - The `environments` array overrides only the keys you specify. It merges into `defaults` and does not replace it. - The timeout chain must be ordered: job `timeout` less than supervisor `timeout` less than `retry_after`. The wrong order can cause jobs to be retried before Horizon finishes timing them out. -- The metrics dashboard stays blank until `horizon:snapshot` is scheduled. Running `vendor/bin/sail artisan horizon` alone does not populate metrics. +- The metrics dashboard stays blank until `horizon:snapshot` is scheduled. Running `php artisan horizon` alone does not populate metrics. - Always use `search-docs` for the latest Horizon documentation rather than relying on this skill alone. \ No newline at end of file diff --git a/.claude/skills/inertia-vue-development/SKILL.md b/.claude/skills/inertia-vue-development/SKILL.md index c69fd98e..a0612246 100644 --- a/.claude/skills/inertia-vue-development/SKILL.md +++ b/.claude/skills/inertia-vue-development/SKILL.md @@ -1,6 +1,6 @@ --- name: inertia-vue-development -description: "Develops Inertia.js v3 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using ,
, useForm, useHttp, setLayoutProps, or router; working with deferred props, prefetching, optimistic updates, instant visits, or polling; or when user mentions Vue with Inertia, Vue pages, Vue forms, or Vue navigation." +description: "Develops Inertia.js v2 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using , , useForm, or router; working with deferred props, prefetching, or polling; or when user mentions Vue with Inertia, Vue pages, Vue forms, or Vue navigation." license: MIT metadata: author: laravel @@ -8,19 +8,9 @@ # Inertia Vue Development -## When to Apply - -Activate this skill when: - -- Creating or modifying Vue page components for Inertia -- Working with forms in Vue (using ``, `useForm`, or `useHttp`) -- Implementing client-side navigation with `` or `router` -- Using v3 features: deferred props, prefetching, optimistic updates, instant visits, layout props, HTTP requests, WhenVisible, InfiniteScroll, once props, flash data, or polling -- Building Vue-specific features with the Inertia protocol - ## Documentation -Use `search-docs` for detailed Inertia v3 Vue patterns and documentation. +Use `search-docs` for detailed Inertia v2 Vue patterns and documentation. ## Basic Usage @@ -30,6 +20,8 @@ ### Page Components Location ### Page Component Structure +Important: Vue components must have a single root element. + ```vue - - -``` - -### Optimistic Updates - -Apply data changes instantly before the server responds, with automatic rollback on failure: - - -```vue - -``` - -Optimistic updates also work with `useForm` and the `` component: - - -```vue - -``` - -### Instant Visits - -Navigate to a new page immediately without waiting for the server response. The target component renders right away with shared props, while page-specific props load in the background. - - -```vue - - - -``` - -### Layout Props - -Share dynamic data between pages and persistent layouts: - - -```vue - - - -``` - - -```vue - - - -``` +## Inertia v2 Features ### Deferred Props @@ -496,69 +358,42 @@ ### Polling ``` -- `autoStart` (default `true`) - set to `false` to start polling manually via the returned `start()` function -- `keepAlive` (default `false`) - set to `true` to prevent throttling when the browser tab is inactive +- `autoStart` (default `true`) — set to `false` to start polling manually via the returned `start()` function +- `keepAlive` (default `false`) — set to `true` to prevent throttling when the browser tab is inactive -### WhenVisible +### WhenVisible (Infinite Scroll) -Lazy-load a prop when an element scrolls into view. Useful for deferring expensive data that sits below the fold: +Load more data when user scrolls to a specific element: - + ```vue - - -``` - -### InfiniteScroll - -Automatically load additional pages of paginated data as users scroll: - - -```vue - ``` -The server must use `Inertia::scroll()` to configure the paginated data. Use the `search-docs` tool with a query of `infinite scroll` for detailed guidance on buffers, manual loading, reverse mode, and custom trigger elements. - ## Server-Side Patterns Server-side patterns (Inertia::render, props, middleware) are covered in inertia-laravel guidelines. @@ -570,6 +405,4 @@ ## Common Pitfalls - Forgetting to add loading states (skeleton screens) when using deferred props - Not handling the `undefined` state of deferred props before data loads - Using `` without preventing default submission (use `` component or `@submit.prevent`) -- Forgetting to check if `` component is available in your Inertia version -- Using `router.cancel()` instead of `router.cancelAll()` (v3 breaking change) -- Using `router.on('invalid', ...)` or `router.on('exception', ...)` instead of the renamed `httpException` and `networkError` events \ No newline at end of file +- Forgetting to check if `` component is available in your Inertia version \ No newline at end of file diff --git a/.claude/skills/mcp-development/SKILL.md b/.claude/skills/mcp-development/SKILL.md new file mode 100644 index 00000000..71269844 --- /dev/null +++ b/.claude/skills/mcp-development/SKILL.md @@ -0,0 +1,155 @@ +--- +name: mcp-development +description: "Use this skill for Laravel MCP development only. Trigger when creating or editing MCP tools, resources, prompts, or servers in Laravel projects. Covers: artisan make:mcp-* generators, mcp:inspector, routes/ai.php, Tool/Resource/Prompt classes, schema validation, shouldRegister(), OAuth setup, URI templates, read-only attributes, and MCP debugging. Do not use for non-Laravel MCP projects or generic AI features without MCP." +license: MIT +metadata: + author: laravel +--- + +# MCP Development + +## Documentation First + +**CRITICAL**: Always use `search-docs` BEFORE writing MCP code. The documentation is version-specific, comprehensive, and always up-to-date. + + +```bash + +# Example searches + +search-docs(['mcp tools', 'mcp resources', 'mcp validation']) +``` + +## Quick Reference + +### Artisan Commands + +Create MCP Primitives" +```bash +php artisan make:mcp-tool ToolName +php artisan make:mcp-resource ResourceName +php artisan make:mcp-prompt PromptName +php artisan make:mcp-server ServerName +``` + +### Basic Tool Implementation + + +```php +use Illuminate\Contracts\JsonSchema\JsonSchema; +use Laravel\Mcp\Request; +use Laravel\Mcp\Response; +use Laravel\Mcp\Server\Tool; + +class MyTool extends Tool +{ + protected string $description = 'Tool description for LLM'; + + public function schema(JsonSchema $schema): array + { + return [ + 'param' => $schema->string()->required(), + ]; + } + + public function handle(Request $request): Response + { + return Response::text($request->get('param')); + } +} +``` + +### Basic Resource Implementation + + +```php +use Laravel\Mcp\Response; +use Laravel\Mcp\Server\Resource; + +class MyResource extends Resource +{ + protected string $description = 'Resource description'; + protected string $uri = 'file://path/to/resource'; + protected string $mimeType = 'text/markdown'; + + public function handle(): Response + { + return Response::text($content); + } +} +``` + +### Response Methods + + +```php +Response::text('Text content'); +Response::error('Error message'); +Response::structured(['key' => 'value']); +``` + +## Testing MCP Primitives + +Test tools, resources, and prompts directly on their server: + + +```php +// Test a tool +$response = MyServer::tool(MyTool::class, ['param' => 'value']); +$response->assertOk()->assertSee('Expected text'); + +// Test as authenticated user +$response = MyServer::actingAs($user)->tool(MyTool::class, [...]); + +// Available assertions +$response->assertOk(); +$response->assertSee('text'); +$response->assertHasErrors(); +$response->assertHasNoErrors(); +$response->assertName('tool-name'); +$response->assertSentNotification('event/type', ['data' => 'value']); +``` + +### MCP Inspector + +Test interactively using the inspector: + + +```bash +php artisan mcp:inspector mcp/my-server # Web server + +php artisan mcp:inspector my-server # Local server + +``` + +## Available Features + +The following features exist—**use `search-docs` for implementation details**: + +- **Tools**: `schema()`, validation, annotations (`#[IsReadOnly]`, `#[IsDestructive]`, etc.) +- **Resources**: URI templates (`HasUriTemplate`), Dynamic resources +- **Prompts**: Arguments, multi-message responses +- **All primitives**: Dependency injection, `shouldRegister()`, validation +- **Responses**: Text, error, structured, streaming, metadata +- **Server registration**: Web routes, local routes, OAuth + +## Critical Imports + + +```php +use Laravel\Mcp\Request; // NOT Laravel\Mcp\Server\Request +use Laravel\Mcp\Response; // NOT Laravel\Mcp\Server\Response +use Laravel\Mcp\Server\Tool; +use Laravel\Mcp\Server\Resource; +use Laravel\Mcp\Server\Prompt; +use Illuminate\Contracts\JsonSchema\JsonSchema; +``` + +## Common Pitfalls + +- **Not using `search-docs` before implementation** +- Wrong imports: `Laravel\Mcp\Server\Request` (wrong) vs `Laravel\Mcp\Request` (correct) +- Forgetting `schema()` method for tools with parameters +- Missing required properties: `$description`, `$uri`, `$mimeType` +- Wrong response pattern: `new Response()` instead of `Response::text()` +- Running `mcp:start` command locally (hangs waiting for stdin) \ No newline at end of file diff --git a/.claude/skills/medialibrary-development/SKILL.md b/.claude/skills/medialibrary-development/SKILL.md new file mode 100644 index 00000000..3f7e1d78 --- /dev/null +++ b/.claude/skills/medialibrary-development/SKILL.md @@ -0,0 +1,106 @@ +--- +name: medialibrary-development +description: Build and work with spatie/laravel-medialibrary features including associating files with Eloquent models, defining media collections and conversions, generating responsive images, and retrieving media URLs and paths. +license: MIT +metadata: + author: Spatie +--- + +# Media Library Development + +## Overview + +Use spatie/laravel-medialibrary to associate files with Eloquent models. Supports image/video conversions, responsive images, multiple collections, and various storage disks. + +## When to Activate + +- Activate when working with file uploads, media attachments, or image processing in Laravel. +- Activate when code references `HasMedia`, `InteractsWithMedia`, the `Media` model, or media collections/conversions. +- Activate when the user wants to add, retrieve, convert, or manage files attached to Eloquent models. + +## Scope + +- In scope: media uploads, collections, conversions, responsive images, custom properties, file retrieval, path/URL generation. +- Out of scope: general file storage without Eloquent association, non-Laravel frameworks. + +## Workflow + +1. Identify the task (model setup, adding media, defining conversions, retrieving files, etc.). +2. Read `references/medialibrary-guide.md` and focus on the relevant section. +3. Apply the patterns from the reference, keeping code minimal and Laravel-native. + +## Core Concepts + +### Model Setup + +Every model that should have media must implement `HasMedia` and use the `InteractsWithMedia` trait: + +```php +use Spatie\MediaLibrary\HasMedia; +use Spatie\MediaLibrary\InteractsWithMedia; + +class BlogPost extends Model implements HasMedia +{ + use InteractsWithMedia; +} +``` + +### Adding Media + +```php +$blogPost->addMedia($file)->toMediaCollection('images'); +$blogPost->addMediaFromUrl($url)->toMediaCollection('images'); +$blogPost->addMediaFromRequest('file')->toMediaCollection('images'); +``` + +### Defining Collections + +```php +public function registerMediaCollections(): void +{ + $this->addMediaCollection('avatar')->singleFile(); + $this->addMediaCollection('downloads')->useDisk('s3'); +} +``` + +### Defining Conversions + +```php +use Spatie\MediaLibrary\MediaCollections\Models\Media; +use Spatie\Image\Enums\Fit; + +public function registerMediaConversions(?Media $media = null): void +{ + $this->addMediaConversion('thumb') + ->fit(Fit::Contain, 300, 300) + ->nonQueued(); +} +``` + +### Retrieving Media + +```php +$url = $model->getFirstMediaUrl('images'); +$thumbUrl = $model->getFirstMediaUrl('images', 'thumb'); +$allMedia = $model->getMedia('images'); +``` + +## Do and Don't + +Do: +- Always implement the `HasMedia` interface alongside the `InteractsWithMedia` trait. +- Use `?Media $media = null` as the parameter for `registerMediaConversions()`. +- Call `->toMediaCollection()` to finalize adding media. +- Use `->nonQueued()` for conversions that should run synchronously. +- Use `->singleFile()` on collections that should only hold one file. +- Use `Spatie\Image\Enums\Fit` enum values for fit methods. + +Don't: +- Don't forget to run `php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"` before migrating. +- Don't use `env()` for disk configuration; use `config()` or set it in `config/media-library.php`. +- Don't call `addMedia()` without calling `toMediaCollection()` — the media won't be saved. +- Don't reference conversion names that aren't registered in `registerMediaConversions()`. + +## References + +- `references/medialibrary-guide.md` \ No newline at end of file diff --git a/.claude/skills/medialibrary-development/references/medialibrary-guide.md b/.claude/skills/medialibrary-development/references/medialibrary-guide.md new file mode 100644 index 00000000..d56d04c9 --- /dev/null +++ b/.claude/skills/medialibrary-development/references/medialibrary-guide.md @@ -0,0 +1,577 @@ +# Laravel Media Library Reference + +Complete reference for `spatie/laravel-medialibrary`. Full documentation: https://spatie.be/docs/laravel-medialibrary + +## Model Setup + +Implement `HasMedia` and use `InteractsWithMedia`: + +```php +use Illuminate\Database\Eloquent\Model; +use Spatie\MediaLibrary\HasMedia; +use Spatie\MediaLibrary\InteractsWithMedia; + +class BlogPost extends Model implements HasMedia +{ + use InteractsWithMedia; + + public function registerMediaCollections(): void + { + $this->addMediaCollection('images'); + } + + public function registerMediaConversions(?Media $media = null): void + { + $this->addMediaConversion('thumb') + ->fit(Fit::Contain, 300, 300); + } +} +``` + +## Adding Media + +### From uploaded file + +```php +$model->addMedia($request->file('image'))->toMediaCollection('images'); +``` + +### From request (shorthand) + +```php +$model->addMediaFromRequest('image')->toMediaCollection('images'); +``` + +### From URL + +```php +$model->addMediaFromUrl('https://example.com/image.jpg')->toMediaCollection('images'); +``` + +### From string content + +```php +$model->addMediaFromString('raw content')->usingFileName('file.txt')->toMediaCollection('files'); +``` + +### From base64 + +```php +$model->addMediaFromBase64($base64Data)->usingFileName('photo.jpg')->toMediaCollection('images'); +``` + +### From stream + +```php +$model->addMediaFromStream($stream)->usingFileName('file.pdf')->toMediaCollection('files'); +``` + +### From existing disk + +```php +$model->addMediaFromDisk('path/to/file.jpg', 's3')->toMediaCollection('images'); +``` + +### Multiple files from request + +```php +$model->addMultipleMediaFromRequest(['images'])->each(function ($fileAdder) { + $fileAdder->toMediaCollection('images'); +}); + +$model->addAllMediaFromRequest()->each(function ($fileAdder) { + $fileAdder->toMediaCollection('images'); +}); +``` + +### Copy instead of move + +```php +$model->copyMedia($pathToFile)->toMediaCollection('images'); +// or +$model->addMedia($pathToFile)->preservingOriginal()->toMediaCollection('images'); +``` + +## FileAdder Options + +All methods are chainable before calling `toMediaCollection()`: + +```php +$model->addMedia($file) + ->usingName('Custom Name') // display name + ->usingFileName('custom-name.jpg') // filename on disk + ->setOrder(3) // order within collection + ->withCustomProperties(['alt' => 'A landscape photo']) + ->withManipulations(['thumb' => ['filter' => 'greyscale']]) + ->withResponsiveImages() // generate responsive variants + ->storingConversionsOnDisk('s3') // put conversions on different disk + ->addCustomHeaders(['CacheControl' => 'max-age=31536000']) + ->toMediaCollection('images'); +``` + +### Store on cloud disk + +```php +$model->addMedia($file)->toMediaCollectionOnCloudDisk('images'); +``` + +## Media Collections + +Define in `registerMediaCollections()`: + +```php +public function registerMediaCollections(): void +{ + // Basic collection + $this->addMediaCollection('images'); + + // Single file (replacing previous on new upload) + $this->addMediaCollection('avatar') + ->singleFile(); + + // Keep only latest N items + $this->addMediaCollection('recent_photos') + ->onlyKeepLatest(5); + + // Specific disk + $this->addMediaCollection('downloads') + ->useDisk('s3'); + + // With conversions disk + $this->addMediaCollection('photos') + ->useDisk('s3') + ->storeConversionsOnDisk('s3-thumbnails'); + + // MIME type restriction + $this->addMediaCollection('documents') + ->acceptsMimeTypes(['application/pdf', 'application/zip']); + + // Custom validation + $this->addMediaCollection('images') + ->acceptsFile(function ($file) { + return $file->mimeType === 'image/jpeg'; + }); + + // Fallback URL/path when collection is empty + $this->addMediaCollection('avatar') + ->singleFile() + ->useFallbackUrl('/images/default-avatar.jpg') + ->useFallbackPath(public_path('/images/default-avatar.jpg')); + + // Enable responsive images for entire collection + $this->addMediaCollection('hero_images') + ->withResponsiveImages(); + + // Collection-specific conversions + $this->addMediaCollection('photos') + ->registerMediaConversions(function () { + $this->addMediaConversion('card') + ->fit(Fit::Crop, 400, 400); + }); +} +``` + +## Media Conversions + +Define in `registerMediaConversions()`: + +```php +use Spatie\MediaLibrary\MediaCollections\Models\Media; +use Spatie\Image\Enums\Fit; + +public function registerMediaConversions(?Media $media = null): void +{ + $this->addMediaConversion('thumb') + ->fit(Fit::Contain, 300, 300) + ->nonQueued(); + + $this->addMediaConversion('preview') + ->fit(Fit::Crop, 500, 500) + ->withResponsiveImages() + ->queued(); + + $this->addMediaConversion('banner') + ->fit(Fit::Max, 1200, 630) + ->performOnCollections('images', 'headers') + ->nonQueued() + ->sharpen(10); + + // Conditional conversion based on media properties + if ($media?->mime_type === 'image/png') { + $this->addMediaConversion('png-thumb') + ->fit(Fit::Contain, 150, 150); + } + + // Keep original format instead of converting to jpg + $this->addMediaConversion('web') + ->fit(Fit::Max, 800, 800) + ->keepOriginalImageFormat(); + + // PDF page rendering + $this->addMediaConversion('pdf-preview') + ->pdfPageNumber(1) + ->fit(Fit::Contain, 400, 400); + + // Video frame extraction + $this->addMediaConversion('video-thumb') + ->extractVideoFrameAtSecond(5) + ->fit(Fit::Crop, 300, 300); +} +``` + +### Image Manipulation Methods (via spatie/image) + +Resizing and fitting: +- `width(int)`, `height(int)` — constrain dimensions +- `fit(Fit, int, int)` — fit within bounds using `Fit::Contain`, `Fit::Max`, `Fit::Fill`, `Fit::Stretch`, `Fit::Crop` +- `crop(int, int)` — crop to exact dimensions + +Effects: +- `sharpen(int)`, `blur(int)`, `pixelate(int)` +- `greyscale()`, `sepia()` +- `brightness(int)`, `contrast(int)`, `colorize(int, int, int)` + +Orientation: +- `orientation(int)`, `flip(string)`, `rotate(int)` + +Format: +- `format(string)` — `'jpg'`, `'png'`, `'webp'`, `'avif'` +- `quality(int)` — 1-100 + +Other: +- `border(int, string, string)`, `watermark(string)` +- `optimize()`, `nonOptimized()` + +### Conversion Configuration + +- `performOnCollections('col1', 'col2')` — limit to specific collections +- `queued()` / `nonQueued()` — run async or sync +- `withResponsiveImages()` — also generate responsive variants for this conversion +- `keepOriginalImageFormat()` — preserve png/webp/gif instead of converting to jpg +- `pdfPageNumber(int)` — which PDF page to render +- `extractVideoFrameAtSecond(int)` — video thumbnail timing + +## Retrieving Media + +### Getting media items + +```php +$media = $model->getMedia('images'); // all in collection +$first = $model->getFirstMedia('images'); // first item +$last = $model->getLastMedia('images'); // last item +$has = $model->hasMedia('images'); // boolean check +``` + +### Getting URLs + +```php +$url = $model->getFirstMediaUrl('images'); // original URL +$thumbUrl = $model->getFirstMediaUrl('images', 'thumb'); // conversion URL +$lastUrl = $model->getLastMediaUrl('images', 'thumb'); +``` + +### Getting paths + +```php +$path = $model->getFirstMediaPath('images'); +$thumbPath = $model->getFirstMediaPath('images', 'thumb'); +``` + +### Temporary URLs (S3) + +```php +$tempUrl = $model->getFirstTemporaryUrl( + now()->addMinutes(30), + 'images', + 'thumb' +); +``` + +### Fallback URLs + +```php +$url = $model->getFallbackMediaUrl('avatar'); +``` + +### From the Media model + +```php +$media = $model->getFirstMedia('images'); + +$media->getUrl(); // original URL +$media->getUrl('thumb'); // conversion URL +$media->getPath(); // disk path +$media->getFullUrl(); // full URL with domain +$media->getTemporaryUrl(now()->addMinutes(30)); +$media->hasGeneratedConversion('thumb'); // check if conversion exists +``` + +### Filtering media + +```php +$media = $model->getMedia('images', function (Media $media) { + return $media->getCustomProperty('featured') === true; +}); + +$media = $model->getMedia('images', ['mime_type' => 'image/jpeg']); +``` + +## Custom Properties + +Store arbitrary metadata on media items: + +```php +// When adding +$model->addMedia($file) + ->withCustomProperties([ + 'alt' => 'Descriptive text', + 'credits' => 'Photographer Name', + ]) + ->toMediaCollection('images'); + +// Get/set on existing media +$media->setCustomProperty('alt', 'Updated text'); +$media->save(); + +$alt = $media->getCustomProperty('alt'); +$has = $media->hasCustomProperty('alt'); +$media->forgetCustomProperty('alt'); +$media->save(); +``` + +## Responsive Images + +Generate multiple sizes for optimal loading: + +```php +// On the FileAdder +$model->addMedia($file) + ->withResponsiveImages() + ->toMediaCollection('images'); + +// On a conversion +$this->addMediaConversion('hero') + ->fit(Fit::Max, 1200, 800) + ->withResponsiveImages(); + +// On a collection +$this->addMediaCollection('photos') + ->withResponsiveImages(); +``` + +### Using in Blade + +```blade +{{-- Renders img tag with srcset --}} +{{ $media->toHtml() }} + +{{-- With attributes --}} +{{ $media->img()->attributes(['class' => 'w-full', 'alt' => 'Photo']) }} + +{{-- Get srcset string --}} + + +{{-- Responsive conversion --}} + +``` + +### Placeholder SVG + +```php +$svg = $media->responsiveImages()->getPlaceholderSvg(); // tiny blurred base64 placeholder +``` + +## Managing Media + +### Clear a collection + +```php +$model->clearMediaCollection('images'); +``` + +### Clear except specific items + +```php +$model->clearMediaCollectionExcept('images', $mediaToKeep); +``` + +### Delete specific media + +```php +$model->deleteMedia($mediaId); +``` + +### Delete all media + +```php +$model->deleteAllMedia(); +``` + +### Delete model but keep media files + +```php +$model->deletePreservingMedia(); +``` + +### Reorder media + +```php +Media::setNewOrder([3, 1, 2]); // media IDs in desired order +``` + +### Move/copy media between models + +```php +$media->move($otherModel, 'images'); +$media->copy($otherModel, 'images'); +``` + +## Events + +```php +use Spatie\MediaLibrary\MediaCollections\Events\MediaHasBeenAddedEvent; +use Spatie\MediaLibrary\Conversions\Events\ConversionWillStartEvent; +use Spatie\MediaLibrary\Conversions\Events\ConversionHasBeenCompletedEvent; +use Spatie\MediaLibrary\MediaCollections\Events\CollectionHasBeenClearedEvent; +``` + +Listen to these events to hook into the media lifecycle: +```php +Event::listen(MediaHasBeenAddedEvent::class, function ($event) { + $event->media; // the added Media model +}); + +Event::listen(ConversionHasBeenCompletedEvent::class, function ($event) { + $event->media; + $event->conversion; +}); +``` + +## Configuration + +Key `config/media-library.php` options: + +```php +return [ + 'disk_name' => 'public', // default disk + 'max_file_size' => 1024 * 1024 * 10, // 10MB + 'queue_connection_name' => '', // queue connection + 'queue_name' => '', // queue name + 'queue_conversions_by_default' => true, // queue conversions + 'media_model' => Spatie\MediaLibrary\MediaCollections\Models\Media::class, + 'file_namer' => Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer::class, + 'path_generator' => Spatie\MediaLibrary\Support\PathGenerator\DefaultPathGenerator::class, + 'url_generator' => Spatie\MediaLibrary\Support\UrlGenerator\DefaultUrlGenerator::class, + 'image_driver' => 'gd', // 'gd', 'imagick', or 'vips' + 'image_optimizers' => [/* optimizer config */], + 'version_urls' => true, // cache busting + 'default_loading_attribute_value' => null, // 'lazy' for lazy loading +]; +``` + +### Custom Path Generator + +```php +use Spatie\MediaLibrary\Support\PathGenerator\PathGenerator; + +class CustomPathGenerator implements PathGenerator +{ + public function getPath(Media $media): string + { + return md5($media->id) . '/'; + } + + public function getPathForConversions(Media $media): string + { + return $this->getPath($media) . 'conversions/'; + } + + public function getPathForResponsiveImages(Media $media): string + { + return $this->getPath($media) . 'responsive/'; + } +} +``` + +### Custom File Namer + +```php +use Spatie\MediaLibrary\Support\FileNamer\FileNamer; + +class CustomFileNamer extends FileNamer +{ + public function originalFileName(string $fileName): string + { + return Str::slug(pathinfo($fileName, PATHINFO_FILENAME)); + } + + public function conversionFileName(string $fileName, Conversion $conversion): string + { + return $this->originalFileName($fileName) . '-' . $conversion->getName(); + } + + public function responsiveFileName(string $fileName): string + { + return pathinfo($fileName, PATHINFO_FILENAME); + } +} +``` + +### Custom Media Model + +```php +use Spatie\MediaLibrary\MediaCollections\Models\Media as BaseMedia; + +class Media extends BaseMedia +{ + // Add custom methods, scopes, or override behavior +} +``` + +Register in config: `'media_model' => App\Models\Media::class` + +## Downloading Media + +### Single file + +```php +return $media->toResponse($request); // download +return $media->toInlineResponse($request); // display inline +return $media->stream(); // stream +``` + +### ZIP download of collection + +```php +use Spatie\MediaLibrary\Support\MediaStream; + +return MediaStream::create('photos.zip') + ->addMedia($model->getMedia('images')); +``` + +## Using with API Resources + +```php +class PostResource extends JsonResource +{ + public function toArray($request): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'image' => $this->getFirstMediaUrl('images'), + 'thumb' => $this->getFirstMediaUrl('images', 'thumb'), + 'media' => $this->getMedia('images')->map(function ($media) { + return [ + 'id' => $media->id, + 'url' => $media->getUrl(), + 'thumb' => $media->getUrl('thumb'), + 'name' => $media->name, + 'size' => $media->size, + 'type' => $media->mime_type, + ]; + }), + ]; + } +} +``` \ No newline at end of file diff --git a/.claude/skills/pennant-development/SKILL.md b/.claude/skills/pennant-development/SKILL.md new file mode 100644 index 00000000..3b364661 --- /dev/null +++ b/.claude/skills/pennant-development/SKILL.md @@ -0,0 +1,77 @@ +--- +name: pennant-development +description: "Use when working with Laravel Pennant the official Laravel feature flag package. Trigger whenever the query mentions Pennant by name or involves feature flags or feature toggles in a Laravel project. Tasks include defining feature flags checking whether features are active creating class based features in `app/Features` using Blade `@feature` directives scoping flags to users or teams building custom Pennant storage drivers protecting routes with feature flags testing feature flags with Pest or PHPUnit and implementing A B testing or gradual rollouts with feature flags. Do not trigger for generic Laravel configuration authorization policies authentication or non Pennant feature management systems." +license: MIT +metadata: + author: laravel +--- + +# Pennant Features + +## When to Apply + +Activate this skill when: + +- Creating or checking feature flags +- Managing feature rollouts +- Implementing A/B testing + +## Documentation + +Use `search-docs` for detailed Pennant patterns and documentation. + +## Basic Usage + +### Defining Features + + +```php +use Laravel\Pennant\Feature; + +Feature::define('new-dashboard', function (User $user) { + return $user->isAdmin(); +}); +``` + +### Checking Features + + +```php +if (Feature::active('new-dashboard')) { + // Feature is active +} + +// With scope +if (Feature::for($user)->active('new-dashboard')) { + // Feature is active for this user +} +``` + +### Blade Directive + + +```blade +@feature('new-dashboard') + +@else + +@endfeature +``` + +### Activating / Deactivating + + +```php +Feature::activate('new-dashboard'); +Feature::for($user)->activate('new-dashboard'); +``` + +## Verification + +1. Check feature flag is defined +2. Test with different scopes/users + +## Common Pitfalls + +- Forgetting to scope features for specific users/entities +- Not following existing naming conventions \ No newline at end of file diff --git a/.claude/skills/pest-testing/SKILL.md b/.claude/skills/pest-testing/SKILL.md index dc6f5d17..ba774e71 100644 --- a/.claude/skills/pest-testing/SKILL.md +++ b/.claude/skills/pest-testing/SKILL.md @@ -16,7 +16,7 @@ ## Basic Usage ### Creating Tests -All tests must be written using Pest. Use `vendor/bin/sail artisan make:test --pest {name}`. +All tests must be written using Pest. Use `php artisan make:test --pest {name}`. ### Test Organization @@ -35,9 +35,9 @@ ### Basic Test Structure ### Running Tests -- Run minimal tests with filter before finalizing: `vendor/bin/sail artisan test --compact --filter=testName`. -- Run all tests: `vendor/bin/sail artisan test --compact`. -- Run file: `vendor/bin/sail artisan test --compact tests/Feature/ExampleTest.php`. +- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`. +- Run all tests: `php artisan test --compact`. +- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`. ## Assertions diff --git a/.claude/skills/upgrade-laravel-v13/SKILL.md b/.claude/skills/upgrade-laravel-v13/SKILL.md new file mode 100644 index 00000000..c7aaa158 --- /dev/null +++ b/.claude/skills/upgrade-laravel-v13/SKILL.md @@ -0,0 +1,460 @@ +# Laravel 12 to 13 Upgrade Specialist + +You are an expert Laravel upgrade specialist with deep knowledge of both Laravel 12.x and 13.0. Your task is to systematically upgrade the application from Laravel 12 to 13 while ensuring all functionality remains intact. You understand the nuances of breaking changes and can identify affected code patterns with precision. + +## Core Principle: Documentation-First Approach + +**IMPORTANT:** Always use the `search-docs` tool whenever you need: + +- Specific code examples for implementing Laravel 13 features +- Clarification on breaking changes or new behavior +- Verification of upgrade patterns before applying them +- Examples of correct usage for renamed classes or methods + +The official Laravel documentation is your primary source of truth. Consult it before making assumptions or implementing changes. + +## Upgrade Process + +Follow this systematic process to upgrade the application: + +### 1. Assess Current State + +Before making any changes: + +- Check `composer.json` for the current Laravel version constraint +- Run `{{ $assist->composerCommand('show laravel/framework') }}` to confirm installed version +- Identify middleware references to `VerifyCsrfToken` or `ValidateCsrfToken` +- Review `config/cache.php` for serialization settings +- Review `config/session.php` for cookie name configuration + +### 2. Create Safety Net + +- Ensure you're working on a dedicated branch +- Run the existing test suite to establish baseline +- Note any custom cache store implementations or queue driver implementations + +### 3. Analyze Codebase for Breaking Changes + +Search the codebase for patterns affected by v13 changes: + +**High Priority Searches:** + +- `VerifyCsrfToken` or `ValidateCsrfToken` — Must rename to `PreventRequestForgery` +- `composer.json` — Dependency version constraints to update +- `phpunit.xml` or `pest` config — Test framework version compatibility + +**Medium Priority Searches:** + +- `config/cache.php` — Check for `serializable_classes` configuration +- Code that stores PHP objects in cache — May need explicit class allow-lists + +**Low Priority Searches:** + +- `$event->exceptionOccurred` — Renamed to `$event->exception` in `JobAttempted` +- `$event->connection` on `QueueBusy` — Renamed to `$connectionName` +- `pagination::default` or `pagination::simple-default` — View names changed +- `Container::call` with nullable class defaults — Behavior changed +- Manager `extend` callbacks using `$this` — Binding changed +- Custom `Str` factories in tests — Now reset between tests + +### 4. Apply Changes Systematically + +For each category of changes: + +1. **Search** for affected patterns using grep/search tools +2. **Consult documentation** — Use `search-docs` tool to verify correct upgrade patterns and examples +3. **List** all files that need modification +4. **Apply** the fix consistently across all occurrences +5. **Verify** each change doesn't break functionality + +### 5. Update Dependencies + +After code changes are complete: + +```bash +{{ $assist->composerCommand('require laravel/framework:^13.0 --with-all-dependencies') }} +``` + +### 6. Test and Verify + +- Run the full test suite +- Verify CSRF protection still works correctly +- Check cache read/write operations +- Test any queue listeners that reference event properties + +## Execution Strategy + +When upgrading, maximize efficiency by: + +- **Batch similar changes** — Group all CSRF middleware renames, then all config updates, etc. +- **Use parallel agents** for independent file modifications +- **Prioritize high-impact changes** that could cause immediate failures +- **Test incrementally** — Verify after each category of changes + +# Upgrading from Laravel 12.x to 13.0 + +> [!NOTE] +> We attempt to document every possible breaking change. Since some of these breaking changes are in obscure parts of the framework only a portion of these changes may actually affect your application. + +## Updating Dependencies + +**Likelihood Of Impact: High** + +Update the following dependencies in your application's `composer.json` file: + +@boostsnippet('Dependency Updates', 'json') +{ +"require": { +"laravel/framework": "^13.0" +}, +"require-dev": { +"laravel/tinker": "^3.0", +"phpunit/phpunit": "^12.0", +"pestphp/pest": "^4.0" +} +} +@endboostsnippet + +Run the update: + +```bash +{{ $assist->composerCommand('update') }} +``` + +## Updating the Laravel Installer + +If you use the Laravel installer CLI tool, update it for Laravel 13.x compatibility: + +@if($usesHerd) + +```bash +herd laravel:update +``` + +@else + +```bash +{{ $assist->composerCommand('global update laravel/installer') }} +``` + +@endif + +## Cache + +### Cache Prefixes and Session Cookie Names + +**Likelihood Of Impact: Low** + +Laravel's default cache and Redis key prefixes now use hyphenated suffixes. In addition, the default session cookie name now uses `Str::snake(...)` for the application name. + +In most applications, this change will not apply because application-level configuration files already define these values. This primarily affects applications that rely on framework-level fallback configuration when corresponding application config values are not present. + +If your application relies on these generated defaults, cache keys and session cookie names may change after upgrading: + +@boostsnippet('Cache Prefix Changes', 'php') +// Laravel <= 12.x +Str::slug((string) env('APP*NAME', 'laravel'), '*').'_cache_'; +Str::slug((string) env('APP*NAME', 'laravel'), '*').'_database_'; +Str::slug((string) env('APP*NAME', 'laravel'), '*').'\_session'; + +// Laravel >= 13.x +Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'; +Str::slug((string) env('APP_NAME', 'laravel')).'-database-'; +Str::snake((string) env('APP_NAME', 'laravel')).'\_session'; +@endboostsnippet + +To retain previous behavior, explicitly configure `CACHE_PREFIX`, `REDIS_PREFIX`, and `SESSION_COOKIE` in your environment. + +### `Store` and `Repository` Contracts: `touch` + +**Likelihood Of Impact: Very Low** + +The cache contracts now include a `touch` method for extending item TTLs. If you maintain custom cache store implementations, you should add this method: + +@boostsnippet('Cache Store Touch', 'php') +// Illuminate\Contracts\Cache\Store +public function touch($key, $seconds); +@endboostsnippet + +### Cache `serializable_classes` Configuration + +**Likelihood Of Impact: Medium** + +The default application `cache` configuration now includes a `serializable_classes` option set to `false`. This hardens cache unserialization behavior to help prevent PHP deserialization gadget chain attacks if your application's `APP_KEY` is leaked. If your application intentionally stores PHP objects in cache, you should explicitly list the classes that may be unserialized: + +@boostsnippet('Cache Serializable Classes', 'php') +'serializable_classes' => [ +App\Data\CachedDashboardStats::class, +App\Support\CachedPricingSnapshot::class, +], +@endboostsnippet + +If your application previously relied on unserializing arbitrary cached objects, you will need to migrate that usage to explicit class allow-lists or to non-object cache payloads (such as arrays). + +## Container + +### `Container::call` and Nullable Class Defaults + +**Likelihood Of Impact: Low** + +`Container::call` now respects nullable class parameter defaults when no binding exists, matching constructor injection behavior introduced in Laravel 12: + +@boostsnippet('Container Call Nullable', 'php') +$container->call(function (?Carbon $date = null) { +return $date; +}); + +// Laravel <= 12.x: Carbon instance +// Laravel >= 13.x: null +@endboostsnippet + +If your method-call injection logic depended on the previous behavior, you may need to update it. + +## Contracts + +### `Dispatcher` Contract: `dispatchAfterResponse` + +**Likelihood Of Impact: Very Low** + +The `Illuminate\Contracts\Bus\Dispatcher` contract now includes the `dispatchAfterResponse($command, $handler = null)` method. + +If you maintain a custom dispatcher implementation, add this method to your class. + +### `ResponseFactory` Contract: `eventStream` + +**Likelihood Of Impact: Very Low** + +The `Illuminate\Contracts\Routing\ResponseFactory` contract now includes an `eventStream` signature. + +If you maintain a custom implementation of this contract, you should add this method. + +### `MustVerifyEmail` Contract: `markEmailAsUnverified` + +**Likelihood Of Impact: Very Low** + +The `Illuminate\Contracts\Auth\MustVerifyEmail` contract now includes `markEmailAsUnverified()`. + +If you provide a custom implementation of this contract, add this method to remain compatible. + +## Database + +### MySQL `DELETE` Queries With `JOIN`, `ORDER BY`, and `LIMIT` + +**Likelihood Of Impact: Low** + +Laravel now compiles full `DELETE ... JOIN` queries including `ORDER BY` and `LIMIT` for MySQL grammar. + +In previous versions, `ORDER BY` / `LIMIT` clauses could be silently ignored on joined deletes. In Laravel 13, these clauses are included in the generated SQL. As a result, database engines that do not support this syntax (such as standard MySQL / MariaDB variants) may now throw a `QueryException` instead of executing an unbounded delete. + +## Eloquent + +### Model Booting and Nested Instantiation + +**Likelihood Of Impact: Very Low** + +Creating a new model instance while that model is still booting is now disallowed and throws a `LogicException`. + +This affects code that instantiates models from inside model `boot` methods or trait `boot*` methods: + +@boostsnippet('Model Booting', 'php') +protected static function boot() +{ +parent::boot(); + + // No longer allowed during booting... + (new static())->getTable(); + +} +@endboostsnippet + +Move this logic outside the boot cycle to avoid nested booting. + +### Polymorphic Pivot Table Name Generation + +**Likelihood Of Impact: Low** + +When table names are inferred for polymorphic pivot models using custom pivot model classes, Laravel now generates pluralized names. + +If your application depended on the previous singular inferred names for morph pivot tables and used custom pivot classes, you should explicitly define the table name on your pivot model. + +### Collection Model Serialization Restores Eager-Loaded Relations + +**Likelihood Of Impact: Low** + +When Eloquent model collections are serialized and restored (such as in queued jobs), eager-loaded relations are now restored for the collection's models. + +If your code depended on relations not being present after deserialization, you may need to adjust that logic. + +## HTTP Client + +### HTTP Client `Response::throw` and `throwIf` Signatures + +**Likelihood Of Impact: Very Low** + +The HTTP client response methods now declare their callback parameters in the method signatures: + +@boostsnippet('HTTP Client Throw Signatures', 'php') +public function throw($callback = null); +public function throwIf($condition, $callback = null); +@endboostsnippet + +If you override these methods in custom response classes, ensure your method signatures are compatible. + +## Notifications + +### Default Password Reset Subject + +**Likelihood Of Impact: Very Low** + +Laravel's default password reset mail subject has changed: + +@boostsnippet('Password Reset Subject', 'text') +// Laravel <= 12.x +Reset Password Notification + +// Laravel >= 13.x +Reset your password +@endboostsnippet + +If your tests, assertions, or translation overrides depend on the previous default string, update them accordingly. + +### Queued Notifications and Missing Models + +**Likelihood Of Impact: Very Low** + +Queued notifications now respect the `#[DeleteWhenMissingModels]` attribute and `$deleteWhenMissingModels` property defined on the notification class. + +In previous versions, missing models could still cause queued notification jobs to fail in cases where you expected them to be deleted. + +## Queue + +### `JobAttempted` Event Exception Payload + +**Likelihood Of Impact: Low** + +The `Illuminate\Queue\Events\JobAttempted` event now exposes the exception object (or `null`) via `$exception`, replacing the previous boolean `$exceptionOccurred` property: + +@boostsnippet('JobAttempted Event', 'php') +// Laravel <= 12.x +$event->exceptionOccurred; + +// Laravel >= 13.x +$event->exception; +@endboostsnippet + +If you listen for this event, update your listener code accordingly. + +### `QueueBusy` Event Property Rename + +**Likelihood Of Impact: Low** + +The `Illuminate\Queue\Events\QueueBusy` event property `$connection` has been renamed to `$connectionName` for consistency with other queue events. + +If your listeners reference `$connection`, update them to `$connectionName`. + +### `Queue` Contract Method Additions + +**Likelihood Of Impact: Very Low** + +The `Illuminate\Contracts\Queue\Queue` contract now includes queue size inspection methods that were previously only declared in docblocks. + +If you maintain custom queue driver implementations of this contract, add implementations for: + +- `pendingSize` +- `delayedSize` +- `reservedSize` +- `creationTimeOfOldestPendingJob` + +## Routing + +### Domain Route Registration Precedence + +**Likelihood Of Impact: Low** + +Routes with an explicit domain are now prioritized before non-domain routes in route matching. + +This allows catch-all subdomain routes to behave consistently even when non-domain routes are registered earlier. If your application relied on previous registration precedence between domain and non-domain routes, review route matching behavior. + +## Scheduling + +### `withScheduling` Registration Timing + +**Likelihood Of Impact: Very Low** + +Schedules registered via `ApplicationBuilder::withScheduling()` are now deferred until `Schedule` is resolved. + +If your application relied on immediate schedule registration timing during bootstrap, you may need to adjust that logic. + +## Security + +### Request Forgery Protection + +**Likelihood Of Impact: High** + +Laravel's CSRF middleware has been renamed from `VerifyCsrfToken` to `PreventRequestForgery`, and now includes request-origin verification using the `Sec-Fetch-Site` header. + +`VerifyCsrfToken` and `ValidateCsrfToken` remain as deprecated aliases, but direct references should be updated to `PreventRequestForgery`, especially when excluding middleware in tests or route definitions: + +@boostsnippet('CSRF Middleware Rename', 'php') +use Illuminate\Foundation\Http\Middleware\PreventRequestForgery; +use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; + +// Laravel <= 12.x +->withoutMiddleware([VerifyCsrfToken::class]); + +// Laravel >= 13.x +->withoutMiddleware([PreventRequestForgery::class]); +@endboostsnippet + +The middleware configuration API now also provides `preventRequestForgery(...)`. + +## Support + +### Manager `extend` Callback Binding + +**Likelihood Of Impact: Low** + +Custom driver closures registered via manager `extend` methods are now bound to the manager instance. + +If you previously relied on another bound object (such as a service provider instance) as `$this` inside these callbacks, you should move those values into closure captures using `use (...)`. + +### `Str` Factories Reset Between Tests + +**Likelihood Of Impact: Low** + +Laravel now resets custom `Str` factories during test teardown. + +If your tests depended on custom UUID / ULID / random string factories persisting between test methods, you should set them in each relevant test or setup hook. + +### `Js::from` Uses Unescaped Unicode By Default + +**Likelihood Of Impact: Very Low** + +`Illuminate\Support\Js::from` now uses `JSON_UNESCAPED_UNICODE` by default. + +If your tests or frontend output comparisons depended on escaped Unicode sequences (for example `\u00e8`), update your expectations. + +## Views + +### Pagination Bootstrap View Names + +**Likelihood Of Impact: Low** + +The internal pagination view names for Bootstrap 3 defaults are now explicit: + +@boostsnippet('Pagination Views', 'text') +// Laravel <= 12.x +pagination::default +pagination::simple-default + +// Laravel >= 13.x +pagination::bootstrap-3 +pagination::simple-bootstrap-3 +@endboostsnippet + +## Getting help + +If you encounter issues during the upgrade: + +- Check the [upgrade guide](https://laravel.com/docs/13.x/upgrade) for the latest details +- Review the [GitHub comparison](https://github.com/laravel/laravel/compare/12.x...13.x) for skeleton changes diff --git a/.claude/skills/wayfinder-development/SKILL.md b/.claude/skills/wayfinder-development/SKILL.md index 451995db..0b306459 100644 --- a/.claude/skills/wayfinder-development/SKILL.md +++ b/.claude/skills/wayfinder-development/SKILL.md @@ -18,11 +18,11 @@ ### Generate Routes Run after route changes if Vite plugin isn't installed: ```bash -vendor/bin/sail artisan wayfinder:generate --no-interaction +php artisan wayfinder:generate --no-interaction ``` For form helpers, use `--with-form` flag: ```bash -vendor/bin/sail artisan wayfinder:generate --with-form --no-interaction +php artisan wayfinder:generate --with-form --no-interaction ``` ### Import Patterns @@ -69,7 +69,7 @@ ## Wayfinder + Inertia ## Verification -1. Run `vendor/bin/sail artisan wayfinder:generate` to regenerate routes if Vite plugin isn't installed +1. Run `php artisan wayfinder:generate` to regenerate routes if Vite plugin isn't installed 2. Check TypeScript imports resolve correctly 3. Verify route URLs match expected paths diff --git a/config/cache.php b/config/cache.php index b32aead2..c68acdfc 100644 --- a/config/cache.php +++ b/config/cache.php @@ -114,4 +114,17 @@ 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), + /* + |-------------------------------------------------------------------------- + | Serializable Classes + |-------------------------------------------------------------------------- + | + | This value determines the classes that can be unserialized from cache + | storage. By default, no PHP classes will be unserialized from your + | cache to prevent gadget chain attacks if your APP_KEY is leaked. + | + */ + + 'serializable_classes' => false, + ]; diff --git a/config/session.php b/config/session.php index 5b541b75..f5744827 100644 --- a/config/session.php +++ b/config/session.php @@ -214,4 +214,20 @@ 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + /* + |-------------------------------------------------------------------------- + | Session Serialization + |-------------------------------------------------------------------------- + | + | This value controls the serialization strategy for session data, which + | is JSON by default. Setting this to "php" allows the storage of PHP + | objects in the session but can make an application vulnerable to + | "gadget chain" serialization attacks if the APP_KEY is leaked. + | + | Supported: "json", "php" + | + */ + + 'serialization' => 'json', + ]; diff --git a/lang/php_en.json b/lang/php_en.json new file mode 100644 index 00000000..156232fe --- /dev/null +++ b/lang/php_en.json @@ -0,0 +1 @@ +{"auth.failed":"These credentials do not match our records.","auth.password":"The provided password is incorrect.","auth.throttle":"Too many login attempts. Please try again in :seconds seconds.","pagination.previous":"« Previous","pagination.next":"Next »","passwords.reset":"Your password has been reset.","passwords.sent":"We have emailed your password reset link.","passwords.throttled":"Please wait before retrying.","passwords.token":"This password reset token is invalid.","passwords.user":"We can't find a user with that email address.","validation.accepted":"The :attribute field must be accepted.","validation.accepted_if":"The :attribute field must be accepted when :other is :value.","validation.active_url":"The :attribute field must be a valid URL.","validation.after":"The :attribute field must be a date after :date.","validation.after_or_equal":"The :attribute field must be a date after or equal to :date.","validation.alpha":"The :attribute field must only contain letters.","validation.alpha_dash":"The :attribute field must only contain letters, numbers, dashes, and underscores.","validation.alpha_num":"The :attribute field must only contain letters and numbers.","validation.any_of":"The :attribute field is invalid.","validation.array":"The :attribute field must be an array.","validation.ascii":"The :attribute field must only contain single-byte alphanumeric characters and symbols.","validation.before":"The :attribute field must be a date before :date.","validation.before_or_equal":"The :attribute field must be a date before or equal to :date.","validation.between.array":"The :attribute field must have between :min and :max items.","validation.between.file":"The :attribute field must be between :min and :max kilobytes.","validation.between.numeric":"The :attribute field must be between :min and :max.","validation.between.string":"The :attribute field must be between :min and :max characters.","validation.boolean":"The :attribute field must be true or false.","validation.can":"The :attribute field contains an unauthorized value.","validation.confirmed":"The :attribute field confirmation does not match.","validation.contains":"The :attribute field is missing a required value.","validation.current_password":"The password is incorrect.","validation.date":"The :attribute field must be a valid date.","validation.date_equals":"The :attribute field must be a date equal to :date.","validation.date_format":"The :attribute field must match the format :format.","validation.decimal":"The :attribute field must have :decimal decimal places.","validation.declined":"The :attribute field must be declined.","validation.declined_if":"The :attribute field must be declined when :other is :value.","validation.different":"The :attribute field and :other must be different.","validation.digits":"The :attribute field must be :digits digits.","validation.digits_between":"The :attribute field must be between :min and :max digits.","validation.dimensions":"The :attribute field has invalid image dimensions.","validation.distinct":"The :attribute field has a duplicate value.","validation.doesnt_contain":"The :attribute field must not contain any of the following: :values.","validation.doesnt_end_with":"The :attribute field must not end with one of the following: :values.","validation.doesnt_start_with":"The :attribute field must not start with one of the following: :values.","validation.email":"The :attribute field must be a valid email address.","validation.encoding":"The :attribute field must be encoded in :encoding.","validation.ends_with":"The :attribute field must end with one of the following: :values.","validation.enum":"The selected :attribute is invalid.","validation.exists":"The selected :attribute is invalid.","validation.extensions":"The :attribute field must have one of the following extensions: :values.","validation.file":"The :attribute field must be a file.","validation.filled":"The :attribute field must have a value.","validation.gt.array":"The :attribute field must have more than :value items.","validation.gt.file":"The :attribute field must be greater than :value kilobytes.","validation.gt.numeric":"The :attribute field must be greater than :value.","validation.gt.string":"The :attribute field must be greater than :value characters.","validation.gte.array":"The :attribute field must have :value items or more.","validation.gte.file":"The :attribute field must be greater than or equal to :value kilobytes.","validation.gte.numeric":"The :attribute field must be greater than or equal to :value.","validation.gte.string":"The :attribute field must be greater than or equal to :value characters.","validation.hex_color":"The :attribute field must be a valid hexadecimal color.","validation.image":"The :attribute field must be an image.","validation.in":"The selected :attribute is invalid.","validation.in_array":"The :attribute field must exist in :other.","validation.in_array_keys":"The :attribute field must contain at least one of the following keys: :values.","validation.integer":"The :attribute field must be an integer.","validation.ip":"The :attribute field must be a valid IP address.","validation.ipv4":"The :attribute field must be a valid IPv4 address.","validation.ipv6":"The :attribute field must be a valid IPv6 address.","validation.json":"The :attribute field must be a valid JSON string.","validation.list":"The :attribute field must be a list.","validation.lowercase":"The :attribute field must be lowercase.","validation.lt.array":"The :attribute field must have less than :value items.","validation.lt.file":"The :attribute field must be less than :value kilobytes.","validation.lt.numeric":"The :attribute field must be less than :value.","validation.lt.string":"The :attribute field must be less than :value characters.","validation.lte.array":"The :attribute field must not have more than :value items.","validation.lte.file":"The :attribute field must be less than or equal to :value kilobytes.","validation.lte.numeric":"The :attribute field must be less than or equal to :value.","validation.lte.string":"The :attribute field must be less than or equal to :value characters.","validation.mac_address":"The :attribute field must be a valid MAC address.","validation.max.array":"The :attribute field must not have more than :max items.","validation.max.file":"The :attribute field must not be greater than :max kilobytes.","validation.max.numeric":"The :attribute field must not be greater than :max.","validation.max.string":"The :attribute field must not be greater than :max characters.","validation.max_digits":"The :attribute field must not have more than :max digits.","validation.mimes":"The :attribute field must be a file of type: :values.","validation.mimetypes":"The :attribute field must be a file of type: :values.","validation.min.array":"The :attribute field must have at least :min items.","validation.min.file":"The :attribute field must be at least :min kilobytes.","validation.min.numeric":"The :attribute field must be at least :min.","validation.min.string":"The :attribute field must be at least :min characters.","validation.min_digits":"The :attribute field must have at least :min digits.","validation.missing":"The :attribute field must be missing.","validation.missing_if":"The :attribute field must be missing when :other is :value.","validation.missing_unless":"The :attribute field must be missing unless :other is :value.","validation.missing_with":"The :attribute field must be missing when :values is present.","validation.missing_with_all":"The :attribute field must be missing when :values are present.","validation.multiple_of":"The :attribute field must be a multiple of :value.","validation.not_in":"The selected :attribute is invalid.","validation.not_regex":"The :attribute field format is invalid.","validation.numeric":"The :attribute field must be a number.","validation.password.letters":"The :attribute field must contain at least one letter.","validation.password.mixed":"The :attribute field must contain at least one uppercase and one lowercase letter.","validation.password.numbers":"The :attribute field must contain at least one number.","validation.password.symbols":"The :attribute field must contain at least one symbol.","validation.password.uncompromised":"The given :attribute has appeared in a data leak. Please choose a different :attribute.","validation.present":"The :attribute field must be present.","validation.present_if":"The :attribute field must be present when :other is :value.","validation.present_unless":"The :attribute field must be present unless :other is :value.","validation.present_with":"The :attribute field must be present when :values is present.","validation.present_with_all":"The :attribute field must be present when :values are present.","validation.prohibited":"The :attribute field is prohibited.","validation.prohibited_if":"The :attribute field is prohibited when :other is :value.","validation.prohibited_if_accepted":"The :attribute field is prohibited when :other is accepted.","validation.prohibited_if_declined":"The :attribute field is prohibited when :other is declined.","validation.prohibited_unless":"The :attribute field is prohibited unless :other is in :values.","validation.prohibits":"The :attribute field prohibits :other from being present.","validation.regex":"The :attribute field format is invalid.","validation.required":"The :attribute field is required.","validation.required_array_keys":"The :attribute field must contain entries for: :values.","validation.required_if":"The :attribute field is required when :other is :value.","validation.required_if_accepted":"The :attribute field is required when :other is accepted.","validation.required_if_declined":"The :attribute field is required when :other is declined.","validation.required_unless":"The :attribute field is required unless :other is in :values.","validation.required_with":"The :attribute field is required when :values is present.","validation.required_with_all":"The :attribute field is required when :values are present.","validation.required_without":"The :attribute field is required when :values is not present.","validation.required_without_all":"The :attribute field is required when none of :values are present.","validation.same":"The :attribute field must match :other.","validation.size.array":"The :attribute field must contain :size items.","validation.size.file":"The :attribute field must be :size kilobytes.","validation.size.numeric":"The :attribute field must be :size.","validation.size.string":"The :attribute field must be :size characters.","validation.starts_with":"The :attribute field must start with one of the following: :values.","validation.string":"The :attribute field must be a string.","validation.timezone":"The :attribute field must be a valid timezone.","validation.unique":"The :attribute has already been taken.","validation.uploaded":"The :attribute failed to upload.","validation.uppercase":"The :attribute field must be uppercase.","validation.url":"The :attribute field must be a valid URL.","validation.ulid":"The :attribute field must be a valid ULID.","validation.uuid":"The :attribute field must be a valid UUID.","validation.custom.attribute-name.rule-name":"custom-message","accounts.title":"Connections","accounts.page_title":"Connected Accounts","accounts.description":"Connect your social networks to schedule and publish posts","accounts.not_connected":"Not connected","accounts.connect":"Connect","accounts.connection_lost":"Connection lost","accounts.reconnect_account":"Reconnect account","accounts.view_profile":"View profile","accounts.disconnect":"Disconnect","accounts.disconnect_modal.title":"Disconnect Account","accounts.disconnect_modal.description":"Are you sure you want to disconnect this account? You can reconnect it at any time.","accounts.disconnect_modal.confirm":"Disconnect","accounts.disconnect_modal.cancel":"Cancel","accounts.bluesky.title":"Connect Bluesky","accounts.bluesky.description":"Enter your credentials to connect","accounts.bluesky.email":"Email","accounts.bluesky.email_placeholder":"yourhandle.bsky.social","accounts.bluesky.app_password":"App Password","accounts.bluesky.app_password_placeholder":"xxxx-xxxx-xxxx-xxxx","accounts.bluesky.app_password_hint":"Use an App Password for security. Create one at bsky.app/settings.","accounts.bluesky.submit":"Connect Bluesky","accounts.bluesky.submitting":"Connecting...","accounts.mastodon.title":"Connect Mastodon","accounts.mastodon.description":"Enter your Mastodon instance","accounts.mastodon.instance_url":"Instance URL","accounts.mastodon.instance_placeholder":"https://mastodon.social","accounts.mastodon.instance_hint":"Enter your Mastodon instance URL (e.g., mastodon.social, techhub.social)","accounts.mastodon.submit":"Continue with Mastodon","accounts.mastodon.submitting":"Connecting...","accounts.facebook.title":"Select Facebook Page","accounts.facebook.description":"Choose which page you want to connect","accounts.facebook.no_pages":"No pages found","accounts.facebook.no_pages_description":"You are not an admin of any Facebook page.","accounts.facebook.page_label":"Facebook Page","accounts.linkedin.title":"Select LinkedIn Page","accounts.linkedin.description":"Choose which page you want to connect","accounts.linkedin.no_pages":"No pages found","accounts.linkedin.no_pages_description":"You are not an administrator of any LinkedIn page.","accounts.linkedin.page_label":"LinkedIn Page","accounts.flash.disconnected":"Account disconnected successfully!","accounts.flash.connected":"Account connected successfully!","accounts.flash.session_expired":"Session expired. Please try again.","accounts.flash.workspace_not_found":"Workspace not found.","accounts.flash.already_connected":"This platform is already connected.","accounts.flash.no_youtube_channels":"No YouTube channels found. Please create a channel first.","auth.flash.welcome":"Welcome to TryPost!","auth.flash.welcome_trial":"Welcome to TryPost! Your trial has started.","auth.login.title":"Log in to your account","auth.login.description":"Enter your email and password below to log in","auth.login.page_title":"Log in","auth.login.email":"Email address","auth.login.password":"Password","auth.login.forgot_password":"Forgot password?","auth.login.remember_me":"Remember me","auth.login.submit":"Log in","auth.login.no_account":"Don't have an account?","auth.login.sign_up":"Sign up","auth.register.title":"Create an account","auth.register.description":"Enter your details below to create your account","auth.register.page_title":"Register","auth.register.name":"Name","auth.register.name_placeholder":"Full name","auth.register.email":"Email address","auth.register.password":"Password","auth.register.submit":"Create account","auth.register.has_account":"Already have an account?","auth.register.log_in":"Log in","auth.forgot_password.title":"Forgot password","auth.forgot_password.description":"Enter your email to receive a password reset link","auth.forgot_password.page_title":"Forgot password","auth.forgot_password.email":"Email address","auth.forgot_password.submit":"Email password reset link","auth.forgot_password.return_to":"Or, return to","auth.forgot_password.log_in":"log in","auth.reset_password.title":"Reset password","auth.reset_password.description":"Please enter your new password below","auth.reset_password.page_title":"Reset password","auth.reset_password.email":"Email","auth.reset_password.password":"Password","auth.reset_password.confirm_password":"Confirm Password","auth.reset_password.confirm_placeholder":"Confirm password","auth.reset_password.submit":"Reset password","auth.verify_email.title":"Verify email","auth.verify_email.description":"Please verify your email address by clicking on the link we just emailed to you.","auth.verify_email.page_title":"Email verification","auth.verify_email.link_sent":"A new verification link has been sent to the email address you provided during registration.","auth.verify_email.resend":"Resend verification email","auth.verify_email.log_out":"Log out","auth.accept_invite.page_title":"Accept Invite","auth.accept_invite.title":"You've been invited!","auth.accept_invite.description":"You've been invited to join the :workspace workspace.","auth.accept_invite.workspace":"Workspace","auth.accept_invite.your_role":"Your role","auth.accept_invite.email":"Email","auth.accept_invite.accept":"Accept Invite","auth.accept_invite.decline":"Decline Invite","auth.accept_invite.login_prompt":"Log in or create an account to accept this invite.","auth.accept_invite.log_in":"Log in","auth.accept_invite.create_account":"Create Account","billing.title":"Subscription","billing.description":"Manage your subscription and payment method","billing.trial.title":"Trial period active","billing.trial.description":"Your trial ends on :date. After that, your subscription will be charged automatically.","billing.subscription.title":"Your Subscription","billing.subscription.workspaces":"Workspaces","billing.subscription.quantity":"Subscription quantity","billing.subscription.expires":"Expires :date","billing.subscription.canceled_on":"Your subscription will be canceled on :date","billing.subscription.manage":"Manage on Stripe","billing.invoices.title":"Invoices","billing.invoices.description":"Payment history","billing.invoices.empty":"No invoices found","billing.invoices.paid":"Paid","billing.status.active":"Active","billing.status.canceled":"Canceled","billing.status.incomplete":"Incomplete","billing.status.incomplete_expired":"Expired","billing.status.past_due":"Past due","billing.status.trialing":"Trial","billing.status.unpaid":"Unpaid","calendar.title":"Calendar","calendar.today":"Today","calendar.day":"Day","calendar.week":"Week","calendar.month":"Month","calendar.new_post":"New Post","calendar.no_content":"No content","calendar.more":"+:count more","hashtags.title":"Hashtags","hashtags.description":"Create hashtag groups to quickly add to your posts","hashtags.new_group":"New Group","hashtags.no_groups_yet":"No hashtag groups yet","hashtags.no_groups_description":"Create hashtag groups to quickly add popular hashtags to your posts","hashtags.create_first_group":"Create your first group","hashtags.hashtags_count":":count hashtags","hashtags.create.title":"Create Hashtag Group","hashtags.create.description":"Give your group a name and add hashtags separated by spaces or commas","hashtags.create.name":"Group Name","hashtags.create.name_placeholder":"e.g. Marketing, Travel, Food","hashtags.create.hashtags":"Hashtags","hashtags.create.hashtags_placeholder":"#marketing #socialmedia #business #growth","hashtags.create.hashtags_hint":"Enter hashtags separated by spaces or commas. Include the # symbol.","hashtags.create.submit":"Create Group","hashtags.create.submitting":"Creating...","hashtags.edit.title":"Edit Hashtag Group","hashtags.edit.description":"Update the name and hashtags for this group","hashtags.edit.name":"Group Name","hashtags.edit.name_placeholder":"e.g. Marketing, Travel, Food","hashtags.edit.hashtags":"Hashtags","hashtags.edit.hashtags_placeholder":"#marketing #socialmedia #business #growth","hashtags.edit.hashtags_hint":"Enter hashtags separated by spaces or commas. Include the # symbol.","hashtags.edit.submit":"Save Changes","hashtags.edit.submitting":"Saving...","hashtags.delete.title":"Delete Hashtag Group","hashtags.delete.description":"Are you sure you want to delete this hashtag group? This action cannot be undone.","hashtags.delete.confirm":"Delete","hashtags.delete.cancel":"Cancel","hashtags.flash.created":"Hashtag group created successfully!","hashtags.flash.updated":"Hashtag group updated successfully!","hashtags.flash.deleted":"Hashtag group deleted successfully!","labels.title":"Labels","labels.description":"Create labels to organize and categorize your posts","labels.new_label":"New Label","labels.no_labels_yet":"No labels yet","labels.create_first_label":"Create your first label","labels.create.title":"Create Label","labels.create.description":"Give your label a name and pick a color","labels.create.name":"Name","labels.create.name_placeholder":"Enter label name...","labels.create.color":"Color","labels.create.submit":"Create Label","labels.create.submitting":"Creating...","labels.edit.title":"Edit Label","labels.edit.description":"Update the name and color for this label","labels.edit.name":"Name","labels.edit.name_placeholder":"Enter label name...","labels.edit.color":"Color","labels.edit.submit":"Save Changes","labels.edit.submitting":"Saving...","labels.delete.title":"Delete Label","labels.delete.description":"Are you sure you want to delete this label? This action cannot be undone.","labels.delete.confirm":"Delete","labels.delete.cancel":"Cancel","labels.flash.created":"Label created successfully!","labels.flash.updated":"Label updated successfully!","labels.flash.deleted":"Label deleted successfully!","mail.workspace_connections_disconnected.subject":"{1} :count account needs to be reconnected in :workspace|[2,*] :count accounts need to be reconnected in :workspace","mail.workspace_connections_disconnected.title":"Accounts Need Reconnection","mail.workspace_connections_disconnected.intro":"The following social accounts in your :workspace workspace have been disconnected and need to be reconnected:","mail.workspace_connections_disconnected.reasons_title":"This may have happened because:","mail.workspace_connections_disconnected.reason_expired":"Access tokens expired","mail.workspace_connections_disconnected.reason_revoked":"You revoked access to TryPost on the platform","mail.workspace_connections_disconnected.reason_changed":"The platform changed their authentication requirements","mail.workspace_connections_disconnected.reconnect_cta":"Please reconnect these accounts to continue scheduling and publishing posts.","mail.workspace_connections_disconnected.button":"Reconnect Accounts","posts.title":"Posts","posts.all_posts":"All Posts","posts.new_post":"New Post","posts.no_posts":"No posts found","posts.start_creating":"Start by creating your first post.","posts.manage_posts":"Manage all your posts","posts.delete_confirm":"Are you sure you want to delete this post?","posts.by":"by","posts.actions.view":"View post","posts.actions.delete":"Delete post","posts.form.post_type":"Post Type","posts.form.board":"Board","posts.form.select_board":"Select a board","posts.form.search_board":"Search board...","posts.form.no_board_found":"No board found","posts.form.media":"Media","posts.form.min":"Min","posts.form.uploading":"Uploading...","posts.form.drop_to_upload":"Drop to upload","posts.form.drag_and_drop":"Drag & drop or click to upload","posts.form.photos_and_videos":"Photos and videos","posts.form.photos_only":"Photos only","posts.form.videos_only":"Videos only","posts.form.drag_to_reorder":"Drag to reorder","posts.form.caption":"Caption","posts.form.write_caption":"Write your caption...","posts.status.draft":"Draft","posts.status.scheduled":"Scheduled","posts.status.publishing":"Publishing","posts.status.published":"Published","posts.status.partially_published":"Partially Published","posts.status.failed":"Failed","posts.descriptions.draft":"Posts waiting to be scheduled","posts.descriptions.scheduled":"Posts scheduled for publishing","posts.descriptions.published":"Posts already published","posts.edit.title":"Edit Post","posts.edit.view_title":"View Post","posts.edit.manage_platforms":"Manage platforms","posts.edit.sync":"Sync","posts.edit.labels":"Labels","posts.edit.hashtags":"Hashtags","posts.edit.schedule":"Schedule","posts.edit.publish":"Publish","posts.edit.delete":"Delete","posts.edit.settings":"Settings","posts.edit.schedule_for":"Schedule for","posts.edit.saving":"Saving...","posts.edit.saved":"Saved","posts.edit.scheduled_at":"Scheduled:","posts.edit.published_at":"Published:","posts.edit.media":"Media","posts.edit.caption":"Caption","posts.edit.no_caption":"No caption","posts.edit.no_content":"No content","posts.edit.empty_state.title":"No platforms selected","posts.edit.empty_state.description":"Select at least one platform to create your post","posts.edit.delete_modal.title":"Delete Post","posts.edit.delete_modal.description":"Are you sure you want to delete this post? This action cannot be undone.","posts.edit.delete_modal.action":"Delete","posts.edit.delete_modal.cancel":"Cancel","posts.edit.sync_enable.title":"Enable sync?","posts.edit.sync_enable.description":"All platforms will share the same content. Any custom edits made to individual platforms will be replaced with the current content.","posts.edit.sync_enable.cancel":"Cancel","posts.edit.sync_enable.action":"Enable sync","posts.edit.sync_disable.title":"Disable sync?","posts.edit.sync_disable.description":"Each platform will keep its current content, but future edits will only apply to the platform you're editing.","posts.edit.sync_disable.customize_note":"You'll be able to customize the content for each platform individually.","posts.edit.sync_disable.cancel":"Cancel","posts.edit.sync_disable.action":"Disable sync","posts.edit.platforms_dialog.title":"Select Platforms","posts.edit.platforms_dialog.description":"Choose which platforms to publish this post to.","posts.edit.hashtags_modal.search":"Search hashtags...","posts.edit.hashtags_modal.no_results":"No hashtags found.","posts.edit.validation.select_board":"Select a board","posts.edit.validation.images_not_supported":"Images not supported","posts.edit.validation.videos_not_supported":"Videos not supported","posts.edit.validation.max_images":"Max :count images","posts.edit.validation.requires_media":"Requires media","posts.edit.validation.exceeded":":count exceeded","posts.edit.validation.does_not_support_images":":platform does not support images","posts.edit.validation.supports_up_to_images":":platform supports up to :count images","posts.edit.validation.does_not_support_videos":":platform does not support videos","posts.content_types.instagram_feed.label":"Feed Post","posts.content_types.instagram_feed.description":"Appears in your feed and profile","posts.content_types.instagram_reel.label":"Reel","posts.content_types.instagram_reel.description":"Short video up to 90 seconds","posts.content_types.instagram_story.label":"Story","posts.content_types.instagram_story.description":"Disappears after 24 hours","posts.content_types.linkedin_post.label":"Post","posts.content_types.linkedin_post.description":"Standard post with text and media","posts.content_types.linkedin_carousel.label":"Carousel","posts.content_types.linkedin_carousel.description":"Swipeable images","posts.content_types.linkedin_page_post.label":"Post","posts.content_types.linkedin_page_post.description":"Standard post with text and media","posts.content_types.linkedin_page_carousel.label":"Carousel","posts.content_types.linkedin_page_carousel.description":"Swipeable images","posts.content_types.facebook_post.label":"Post","posts.content_types.facebook_post.description":"Standard post on your page","posts.content_types.facebook_reel.label":"Reel","posts.content_types.facebook_reel.description":"Short video up to 90 seconds","posts.content_types.facebook_story.label":"Story","posts.content_types.facebook_story.description":"Disappears after 24 hours","posts.content_types.tiktok_video.label":"Video","posts.content_types.tiktok_video.description":"Short-form video content","posts.content_types.youtube_short.label":"Short","posts.content_types.youtube_short.description":"Vertical video up to 60 seconds","posts.content_types.x_post.label":"Post","posts.content_types.x_post.description":"Tweet with text and media","posts.content_types.threads_post.label":"Post","posts.content_types.threads_post.description":"Text post with optional media","posts.content_types.pinterest_pin.label":"Pin","posts.content_types.pinterest_pin.description":"Image pin with link","posts.content_types.pinterest_video_pin.label":"Video Pin","posts.content_types.pinterest_video_pin.description":"Video content","posts.content_types.pinterest_carousel.label":"Carousel","posts.content_types.pinterest_carousel.description":"2-5 images","posts.content_types.bluesky_post.label":"Post","posts.content_types.bluesky_post.description":"Text post with optional images","posts.content_types.mastodon_post.label":"Post","posts.content_types.mastodon_post.description":"Text post with optional media","posts.platforms.linkedin":"LinkedIn","posts.platforms.linkedin-page":"LinkedIn Page","posts.platforms.x":"X","posts.platforms.tiktok":"TikTok","posts.platforms.youtube":"YouTube Shorts","posts.platforms.facebook":"Facebook Page","posts.platforms.instagram":"Instagram","posts.platforms.threads":"Threads","posts.platforms.pinterest":"Pinterest","posts.platforms.bluesky":"Bluesky","posts.platforms.mastodon":"Mastodon","posts.flash.scheduled":"Post scheduled successfully!","posts.flash.publishing":"Post is being published!","posts.flash.deleted":"Post deleted successfully!","posts.flash.cannot_edit_published":"Published posts cannot be edited.","posts.flash.connect_first":"Connect at least one social network before creating a post.","posts.errors.account_disconnected":"Social account is disconnected","settings.title":"Settings","settings.description":"Manage your profile and account settings","settings.nav.profile":"Profile","settings.nav.password":"Password","settings.nav.workspace":"Workspace","settings.nav.members":"Members","settings.nav.billing":"Billing","settings.profile.title":"Profile settings","settings.profile.heading":"Profile information","settings.profile.description":"Update your name and email address","settings.profile.avatar":"Avatar","settings.profile.name":"Name","settings.profile.name_placeholder":"Full name","settings.profile.email":"Email address","settings.profile.email_placeholder":"Email address","settings.profile.email_unverified":"Your email address is unverified.","settings.profile.resend_verification":"Click here to resend the verification email.","settings.profile.verification_sent":"A new verification link has been sent to your email address.","settings.profile.save":"Save","settings.password.title":"Password settings","settings.password.heading":"Update password","settings.password.description":"Ensure your account is using a long, random password to stay secure","settings.password.current_password":"Current password","settings.password.current_password_placeholder":"Current password","settings.password.new_password":"New password","settings.password.new_password_placeholder":"New password","settings.password.confirm_password":"Confirm password","settings.password.confirm_password_placeholder":"Confirm password","settings.password.save":"Save password","settings.delete_account.heading":"Delete account","settings.delete_account.description":"Delete your account and all of its resources","settings.delete_account.warning":"Warning","settings.delete_account.warning_message":"Please proceed with caution, this cannot be undone.","settings.delete_account.button":"Delete account","settings.delete_account.modal_title":"Are you sure you want to delete your account?","settings.delete_account.modal_description":"Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.","settings.delete_account.password":"Password","settings.delete_account.password_placeholder":"Password","settings.delete_account.cancel":"Cancel","settings.delete_account.confirm":"Delete account","settings.workspace.title":"Workspace settings","settings.workspace.heading":"Workspace settings","settings.workspace.description":"Update your workspace name, logo, and timezone","settings.workspace.logo":"Logo","settings.workspace.name":"Name","settings.workspace.name_placeholder":"My Workspace","settings.workspace.timezone":"Timezone","settings.workspace.save":"Save","settings.workspace.saved":"Saved.","settings.members.title":"Members","settings.members.heading":"Team members","settings.members.description":"Manage members and invites for this workspace","settings.members.invite.title":"Invite Member","settings.members.invite.description":"Send an email invite to add collaborators","settings.members.invite.email":"Email","settings.members.invite.email_placeholder":"collaborator@email.com","settings.members.invite.role":"Role","settings.members.invite.role_placeholder":"Select a role","settings.members.invite.submit":"Send Invite","settings.members.invite.cancel_confirm":"Are you sure you want to cancel this invite?","settings.members.pending.title":"Pending Invites","settings.members.pending.description":"Invites awaiting acceptance","settings.members.pending.empty":"No pending invites","settings.members.list.title":"Members","settings.members.list.description":"People with access to this workspace","settings.members.list.empty":"No members besides the owner","settings.members.list.remove_confirm":"Are you sure you want to remove this member?","settings.members.roles.owner":"Owner","settings.members.roles.admin":"Admin","settings.members.roles.member":"Member","settings.members.flash.invite_sent":"Invite sent successfully!","settings.members.flash.invite_deleted":"Invite deleted.","settings.members.flash.member_removed":"Member removed successfully.","settings.members.flash.wrong_email":"This invite is for a different email address.","settings.members.flash.already_member":"You are already a member of this workspace.","settings.members.flash.invite_accepted":"Welcome! You are now a member of the workspace.","settings.members.flash.invite_declined":"Invite declined.","settings.flash.profile_updated":"Profile updated successfully!","settings.flash.language_updated":"Language updated successfully!","settings.flash.password_updated":"Password updated successfully!","settings.flash.workspace_updated":"Settings updated successfully!","sidebar.select_workspace":"Select workspace","sidebar.create_workspace":"Create workspace","sidebar.create_post":"Create post","sidebar.profile":"Profile","sidebar.log_out":"Log out","sidebar.workspace":"Workspace: :name","sidebar.workspace_select":"Workspace: Select","sidebar.theme":"Theme: :name","sidebar.theme_light":"Light","sidebar.theme_dark":"Dark","sidebar.theme_system":"System","sidebar.language":"Language: :name","sidebar.language_select":"Language: Select","sidebar.groups.posts":"Posts","sidebar.groups.configuration":"Configurations","sidebar.groups.support":"Support","sidebar.posts.calendar":"Calendar","sidebar.posts.all":"All","sidebar.posts.scheduled":"Scheduled","sidebar.posts.posted":"Posted","sidebar.posts.drafts":"Drafts","sidebar.config.connections":"Connections","sidebar.config.hashtags":"Hashtags","sidebar.config.labels":"Labels","sidebar.config.settings":"Settings","sidebar.support.discord":"Discord","sidebar.support.share_feedback":"Share feedback","sidebar.support.last_updates":"Last Updates","sidebar.support.docs":"Docs"} \ No newline at end of file diff --git a/lang/php_pt-br.json b/lang/php_pt-br.json new file mode 100644 index 00000000..b71de77d --- /dev/null +++ b/lang/php_pt-br.json @@ -0,0 +1 @@ +{"accounts.title":"Conexões","accounts.page_title":"Contas Conectadas","accounts.description":"Conecte suas redes sociais para agendar e publicar posts","accounts.not_connected":"Não conectado","accounts.connect":"Conectar","accounts.connection_lost":"Conexão perdida","accounts.reconnect_account":"Reconectar conta","accounts.view_profile":"Ver perfil","accounts.disconnect":"Desconectar","accounts.disconnect_modal.title":"Desconectar Conta","accounts.disconnect_modal.description":"Tem certeza que deseja desconectar esta conta? Você pode reconectá-la a qualquer momento.","accounts.disconnect_modal.confirm":"Desconectar","accounts.disconnect_modal.cancel":"Cancelar","accounts.bluesky.title":"Conectar Bluesky","accounts.bluesky.description":"Digite suas credenciais para conectar","accounts.bluesky.email":"E-mail","accounts.bluesky.email_placeholder":"seuhandle.bsky.social","accounts.bluesky.app_password":"Senha do App","accounts.bluesky.app_password_placeholder":"xxxx-xxxx-xxxx-xxxx","accounts.bluesky.app_password_hint":"Use uma Senha do App por segurança. Crie uma em bsky.app/settings.","accounts.bluesky.submit":"Conectar Bluesky","accounts.bluesky.submitting":"Conectando...","accounts.mastodon.title":"Conectar Mastodon","accounts.mastodon.description":"Digite a instância do seu Mastodon","accounts.mastodon.instance_url":"URL da Instância","accounts.mastodon.instance_placeholder":"https://mastodon.social","accounts.mastodon.instance_hint":"Digite a URL da sua instância Mastodon (ex: mastodon.social, techhub.social)","accounts.mastodon.submit":"Continuar com Mastodon","accounts.mastodon.submitting":"Conectando...","accounts.facebook.title":"Selecionar Página do Facebook","accounts.facebook.description":"Escolha qual página você deseja conectar","accounts.facebook.no_pages":"Nenhuma página encontrada","accounts.facebook.no_pages_description":"Você não é administrador de nenhuma página do Facebook.","accounts.facebook.page_label":"Página do Facebook","accounts.linkedin.title":"Selecionar Página do LinkedIn","accounts.linkedin.description":"Escolha qual página você deseja conectar","accounts.linkedin.no_pages":"Nenhuma página encontrada","accounts.linkedin.no_pages_description":"Você não é administrador de nenhuma página do LinkedIn.","accounts.linkedin.page_label":"Página do LinkedIn","accounts.flash.disconnected":"Conta desconectada com sucesso!","accounts.flash.connected":"Conta conectada com sucesso!","accounts.flash.session_expired":"Sessão expirada. Por favor, tente novamente.","accounts.flash.workspace_not_found":"Workspace não encontrado.","accounts.flash.already_connected":"Esta plataforma já está conectada.","accounts.flash.no_youtube_channels":"Nenhum canal do YouTube encontrado. Por favor, crie um canal primeiro.","auth.failed":"Essas credenciais não correspondem aos nossos registros.","auth.password":"A senha fornecida está incorreta.","auth.throttle":"Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.","auth.flash.welcome":"Bem-vindo ao TryPost!","auth.flash.welcome_trial":"Bem-vindo ao TryPost! Seu período de teste começou.","auth.login.title":"Entrar na sua conta","auth.login.description":"Digite seu email e senha abaixo para entrar","auth.login.page_title":"Entrar","auth.login.email":"Endereço de email","auth.login.password":"Senha","auth.login.forgot_password":"Esqueceu a senha?","auth.login.remember_me":"Lembrar de mim","auth.login.submit":"Entrar","auth.login.no_account":"Não tem uma conta?","auth.login.sign_up":"Cadastre-se","auth.register.title":"Criar uma conta","auth.register.description":"Digite seus dados abaixo para criar sua conta","auth.register.page_title":"Cadastro","auth.register.name":"Nome","auth.register.name_placeholder":"Nome completo","auth.register.email":"Endereço de email","auth.register.password":"Senha","auth.register.submit":"Criar conta","auth.register.has_account":"Já tem uma conta?","auth.register.log_in":"Entrar","auth.forgot_password.title":"Esqueceu a senha","auth.forgot_password.description":"Digite seu email para receber um link de redefinição de senha","auth.forgot_password.page_title":"Esqueceu a senha","auth.forgot_password.email":"Endereço de email","auth.forgot_password.submit":"Enviar link de redefinição","auth.forgot_password.return_to":"Ou, volte para","auth.forgot_password.log_in":"entrar","auth.reset_password.title":"Redefinir senha","auth.reset_password.description":"Por favor, digite sua nova senha abaixo","auth.reset_password.page_title":"Redefinir senha","auth.reset_password.email":"Email","auth.reset_password.password":"Senha","auth.reset_password.confirm_password":"Confirmar Senha","auth.reset_password.confirm_placeholder":"Confirmar senha","auth.reset_password.submit":"Redefinir senha","auth.verify_email.title":"Verificar email","auth.verify_email.description":"Por favor, verifique seu endereço de email clicando no link que acabamos de enviar.","auth.verify_email.page_title":"Verificação de email","auth.verify_email.link_sent":"Um novo link de verificação foi enviado para o endereço de email que você forneceu durante o cadastro.","auth.verify_email.resend":"Reenviar email de verificação","auth.verify_email.log_out":"Sair","auth.accept_invite.page_title":"Aceitar Convite","auth.accept_invite.title":"Você foi convidado!","auth.accept_invite.description":"Você foi convidado para participar do workspace :workspace.","auth.accept_invite.workspace":"Workspace","auth.accept_invite.your_role":"Seu cargo","auth.accept_invite.email":"Email","auth.accept_invite.accept":"Aceitar Convite","auth.accept_invite.decline":"Recusar Convite","auth.accept_invite.login_prompt":"Entre ou crie uma conta para aceitar este convite.","auth.accept_invite.log_in":"Entrar","auth.accept_invite.create_account":"Criar Conta","billing.title":"Assinatura","billing.description":"Gerencie sua assinatura e método de pagamento","billing.trial.title":"Período de teste ativo","billing.trial.description":"Seu período de teste termina em :date. Após isso, sua assinatura será cobrada automaticamente.","billing.subscription.title":"Sua Assinatura","billing.subscription.workspaces":"Workspaces","billing.subscription.quantity":"Quantidade da assinatura","billing.subscription.expires":"Expira em :date","billing.subscription.canceled_on":"Sua assinatura será cancelada em :date","billing.subscription.manage":"Gerenciar no Stripe","billing.invoices.title":"Faturas","billing.invoices.description":"Histórico de pagamentos","billing.invoices.empty":"Nenhuma fatura encontrada","billing.invoices.paid":"Pago","billing.status.active":"Ativo","billing.status.canceled":"Cancelado","billing.status.incomplete":"Incompleto","billing.status.incomplete_expired":"Expirado","billing.status.past_due":"Vencido","billing.status.trialing":"Teste","billing.status.unpaid":"Não pago","calendar.title":"Calendário","calendar.today":"Hoje","calendar.day":"Dia","calendar.week":"Semana","calendar.month":"Mês","calendar.new_post":"Novo Post","calendar.no_content":"Sem conteúdo","calendar.more":"+:count mais","hashtags.title":"Hashtags","hashtags.description":"Crie grupos de hashtags para adicionar rapidamente aos seus posts","hashtags.new_group":"Novo Grupo","hashtags.no_groups_yet":"Nenhum grupo de hashtags ainda","hashtags.no_groups_description":"Crie grupos de hashtags para adicionar rapidamente hashtags populares aos seus posts","hashtags.create_first_group":"Crie seu primeiro grupo","hashtags.hashtags_count":":count hashtags","hashtags.create.title":"Criar Grupo de Hashtags","hashtags.create.description":"Dê um nome ao grupo e adicione hashtags separadas por espaços ou vírgulas","hashtags.create.name":"Nome do Grupo","hashtags.create.name_placeholder":"ex: Marketing, Viagem, Comida","hashtags.create.hashtags":"Hashtags","hashtags.create.hashtags_placeholder":"#marketing #redessociais #negocios #crescimento","hashtags.create.hashtags_hint":"Digite as hashtags separadas por espaços ou vírgulas. Inclua o símbolo #.","hashtags.create.submit":"Criar Grupo","hashtags.create.submitting":"Criando...","hashtags.edit.title":"Editar Grupo de Hashtags","hashtags.edit.description":"Atualize o nome e as hashtags deste grupo","hashtags.edit.name":"Nome do Grupo","hashtags.edit.name_placeholder":"ex: Marketing, Viagem, Comida","hashtags.edit.hashtags":"Hashtags","hashtags.edit.hashtags_placeholder":"#marketing #redessociais #negocios #crescimento","hashtags.edit.hashtags_hint":"Digite as hashtags separadas por espaços ou vírgulas. Inclua o símbolo #.","hashtags.edit.submit":"Salvar Alterações","hashtags.edit.submitting":"Salvando...","hashtags.delete.title":"Excluir Grupo de Hashtags","hashtags.delete.description":"Tem certeza que deseja excluir este grupo de hashtags? Esta ação não pode ser desfeita.","hashtags.delete.confirm":"Excluir","hashtags.delete.cancel":"Cancelar","hashtags.flash.created":"Grupo de hashtags criado com sucesso!","hashtags.flash.updated":"Grupo de hashtags atualizado com sucesso!","hashtags.flash.deleted":"Grupo de hashtags excluído com sucesso!","labels.title":"Etiquetas","labels.description":"Crie etiquetas para organizar e categorizar seus posts","labels.new_label":"Nova Etiqueta","labels.no_labels_yet":"Nenhuma etiqueta ainda","labels.create_first_label":"Crie sua primeira etiqueta","labels.create.title":"Criar Etiqueta","labels.create.description":"Dê um nome e escolha uma cor para sua etiqueta","labels.create.name":"Nome","labels.create.name_placeholder":"Digite o nome da etiqueta...","labels.create.color":"Cor","labels.create.submit":"Criar Etiqueta","labels.create.submitting":"Criando...","labels.edit.title":"Editar Etiqueta","labels.edit.description":"Atualize o nome e a cor desta etiqueta","labels.edit.name":"Nome","labels.edit.name_placeholder":"Digite o nome da etiqueta...","labels.edit.color":"Cor","labels.edit.submit":"Salvar Alterações","labels.edit.submitting":"Salvando...","labels.delete.title":"Excluir Etiqueta","labels.delete.description":"Tem certeza que deseja excluir esta etiqueta? Esta ação não pode ser desfeita.","labels.delete.confirm":"Excluir","labels.delete.cancel":"Cancelar","labels.flash.created":"Etiqueta criada com sucesso!","labels.flash.updated":"Etiqueta atualizada com sucesso!","labels.flash.deleted":"Etiqueta excluída com sucesso!","mail.workspace_connections_disconnected.subject":"{1} :count conta precisa ser reconectada em :workspace|[2,*] :count contas precisam ser reconectadas em :workspace","mail.workspace_connections_disconnected.title":"Contas Precisam ser Reconectadas","mail.workspace_connections_disconnected.intro":"As seguintes contas de redes sociais no seu workspace :workspace foram desconectadas e precisam ser reconectadas:","mail.workspace_connections_disconnected.reasons_title":"Isso pode ter acontecido porque:","mail.workspace_connections_disconnected.reason_expired":"Os tokens de acesso expiraram","mail.workspace_connections_disconnected.reason_revoked":"Você revogou o acesso ao TryPost na plataforma","mail.workspace_connections_disconnected.reason_changed":"A plataforma mudou os requisitos de autenticação","mail.workspace_connections_disconnected.reconnect_cta":"Por favor, reconecte essas contas para continuar agendando e publicando posts.","mail.workspace_connections_disconnected.button":"Reconectar Contas","pagination.previous":"« Anterior","pagination.next":"Próximo »","passwords.reset":"Sua senha foi redefinida.","passwords.sent":"Enviamos o link de redefinição de senha por e-mail.","passwords.throttled":"Por favor, aguarde antes de tentar novamente.","passwords.token":"Este token de redefinição de senha é inválido.","passwords.user":"Não conseguimos encontrar um usuário com esse endereço de e-mail.","posts.title":"Posts","posts.all_posts":"Todos os Posts","posts.new_post":"Novo Post","posts.no_posts":"Nenhum post encontrado","posts.start_creating":"Comece criando seu primeiro post.","posts.manage_posts":"Gerencie todos os seus posts","posts.delete_confirm":"Tem certeza que deseja excluir este post?","posts.by":"por","posts.actions.view":"Ver post","posts.actions.delete":"Excluir post","posts.form.post_type":"Tipo de Post","posts.form.board":"Pasta","posts.form.select_board":"Selecione uma pasta","posts.form.search_board":"Buscar pasta...","posts.form.no_board_found":"Nenhuma pasta encontrada","posts.form.media":"Mídia","posts.form.min":"Mín","posts.form.uploading":"Enviando...","posts.form.drop_to_upload":"Solte para enviar","posts.form.drag_and_drop":"Arraste e solte ou clique para enviar","posts.form.photos_and_videos":"Fotos e vídeos","posts.form.photos_only":"Apenas fotos","posts.form.videos_only":"Apenas vídeos","posts.form.drag_to_reorder":"Arraste para reordenar","posts.form.caption":"Legenda","posts.form.write_caption":"Escreva sua legenda...","posts.status.draft":"Rascunho","posts.status.scheduled":"Agendado","posts.status.publishing":"Publicando","posts.status.published":"Publicado","posts.status.partially_published":"Parcialmente Publicado","posts.status.failed":"Falhou","posts.descriptions.draft":"Posts aguardando agendamento","posts.descriptions.scheduled":"Posts agendados para publicação","posts.descriptions.published":"Posts já publicados","posts.edit.title":"Editar Post","posts.edit.view_title":"Visualizar Post","posts.edit.manage_platforms":"Gerenciar plataformas","posts.edit.sync":"Sincronizar","posts.edit.labels":"Etiquetas","posts.edit.hashtags":"Hashtags","posts.edit.schedule":"Agendar","posts.edit.publish":"Publicar","posts.edit.delete":"Excluir","posts.edit.settings":"Configurações","posts.edit.schedule_for":"Agendar para","posts.edit.saving":"Salvando...","posts.edit.saved":"Salvo","posts.edit.scheduled_at":"Agendado:","posts.edit.published_at":"Publicado:","posts.edit.media":"Mídia","posts.edit.caption":"Legenda","posts.edit.no_caption":"Sem legenda","posts.edit.no_content":"Sem conteúdo","posts.edit.empty_state.title":"Nenhuma plataforma selecionada","posts.edit.empty_state.description":"Selecione pelo menos uma plataforma para criar seu post","posts.edit.delete_modal.title":"Excluir Post","posts.edit.delete_modal.description":"Tem certeza que deseja excluir este post? Esta ação não pode ser desfeita.","posts.edit.delete_modal.action":"Excluir","posts.edit.delete_modal.cancel":"Cancelar","posts.edit.sync_enable.title":"Ativar sincronização?","posts.edit.sync_enable.description":"Todas as plataformas compartilharão o mesmo conteúdo. Qualquer edição personalizada feita em plataformas individuais será substituída pelo conteúdo atual.","posts.edit.sync_enable.cancel":"Cancelar","posts.edit.sync_enable.action":"Ativar sincronização","posts.edit.sync_disable.title":"Desativar sincronização?","posts.edit.sync_disable.description":"Cada plataforma manterá seu conteúdo atual, mas edições futuras serão aplicadas apenas à plataforma que você estiver editando.","posts.edit.sync_disable.customize_note":"Você poderá personalizar o conteúdo para cada plataforma individualmente.","posts.edit.sync_disable.cancel":"Cancelar","posts.edit.sync_disable.action":"Desativar sincronização","posts.edit.platforms_dialog.title":"Selecionar Plataformas","posts.edit.platforms_dialog.description":"Escolha em quais plataformas publicar este post.","posts.edit.hashtags_modal.search":"Buscar hashtags...","posts.edit.hashtags_modal.no_results":"Nenhuma hashtag encontrada.","posts.edit.validation.select_board":"Selecione uma pasta","posts.edit.validation.images_not_supported":"Imagens não suportadas","posts.edit.validation.videos_not_supported":"Vídeos não suportados","posts.edit.validation.max_images":"Máx :count imagens","posts.edit.validation.requires_media":"Requer mídia","posts.edit.validation.exceeded":":count excedido","posts.edit.validation.does_not_support_images":":platform não suporta imagens","posts.edit.validation.supports_up_to_images":":platform suporta até :count imagens","posts.edit.validation.does_not_support_videos":":platform não suporta vídeos","posts.content_types.instagram_feed.label":"Post do Feed","posts.content_types.instagram_feed.description":"Aparece no seu feed e perfil","posts.content_types.instagram_reel.label":"Reels","posts.content_types.instagram_reel.description":"Vídeo curto de até 90 segundos","posts.content_types.instagram_story.label":"Story","posts.content_types.instagram_story.description":"Desaparece após 24 horas","posts.content_types.linkedin_post.label":"Post","posts.content_types.linkedin_post.description":"Post padrão com texto e mídia","posts.content_types.linkedin_carousel.label":"Carrossel","posts.content_types.linkedin_carousel.description":"Imagens deslizáveis","posts.content_types.linkedin_page_post.label":"Post","posts.content_types.linkedin_page_post.description":"Post padrão com texto e mídia","posts.content_types.linkedin_page_carousel.label":"Carrossel","posts.content_types.linkedin_page_carousel.description":"Imagens deslizáveis","posts.content_types.facebook_post.label":"Post","posts.content_types.facebook_post.description":"Post padrão na sua página","posts.content_types.facebook_reel.label":"Reels","posts.content_types.facebook_reel.description":"Vídeo curto de até 90 segundos","posts.content_types.facebook_story.label":"Story","posts.content_types.facebook_story.description":"Desaparece após 24 horas","posts.content_types.tiktok_video.label":"Vídeo","posts.content_types.tiktok_video.description":"Conteúdo de vídeo curto","posts.content_types.youtube_short.label":"Short","posts.content_types.youtube_short.description":"Vídeo vertical de até 60 segundos","posts.content_types.x_post.label":"Post","posts.content_types.x_post.description":"Tweet com texto e mídia","posts.content_types.threads_post.label":"Post","posts.content_types.threads_post.description":"Post de texto com mídia opcional","posts.content_types.pinterest_pin.label":"Pin","posts.content_types.pinterest_pin.description":"Pin de imagem com link","posts.content_types.pinterest_video_pin.label":"Pin de Vídeo","posts.content_types.pinterest_video_pin.description":"Conteúdo em vídeo","posts.content_types.pinterest_carousel.label":"Carrossel","posts.content_types.pinterest_carousel.description":"2-5 imagens","posts.content_types.bluesky_post.label":"Post","posts.content_types.bluesky_post.description":"Post de texto com imagens opcionais","posts.content_types.mastodon_post.label":"Post","posts.content_types.mastodon_post.description":"Post de texto com mídia opcional","posts.platforms.linkedin":"LinkedIn","posts.platforms.linkedin-page":"Página do LinkedIn","posts.platforms.x":"X","posts.platforms.tiktok":"TikTok","posts.platforms.youtube":"YouTube Shorts","posts.platforms.facebook":"Página do Facebook","posts.platforms.instagram":"Instagram","posts.platforms.threads":"Threads","posts.platforms.pinterest":"Pinterest","posts.platforms.bluesky":"Bluesky","posts.platforms.mastodon":"Mastodon","posts.flash.scheduled":"Post agendado com sucesso!","posts.flash.publishing":"Post está sendo publicado!","posts.flash.deleted":"Post excluído com sucesso!","posts.flash.cannot_edit_published":"Posts publicados não podem ser editados.","posts.flash.connect_first":"Conecte pelo menos uma rede social antes de criar um post.","posts.errors.account_disconnected":"Conta social está desconectada","settings.title":"Configurações","settings.description":"Gerencie seu perfil e configurações da conta","settings.nav.profile":"Perfil","settings.nav.password":"Senha","settings.nav.workspace":"Workspace","settings.nav.members":"Membros","settings.nav.billing":"Faturamento","settings.profile.title":"Configurações do perfil","settings.profile.heading":"Informações do perfil","settings.profile.description":"Atualize seu nome e endereço de e-mail","settings.profile.avatar":"Avatar","settings.profile.name":"Nome","settings.profile.name_placeholder":"Nome completo","settings.profile.email":"Endereço de e-mail","settings.profile.email_placeholder":"Endereço de e-mail","settings.profile.email_unverified":"Seu endereço de e-mail não foi verificado.","settings.profile.resend_verification":"Clique aqui para reenviar o e-mail de verificação.","settings.profile.verification_sent":"Um novo link de verificação foi enviado para seu endereço de e-mail.","settings.profile.save":"Salvar","settings.password.title":"Configurações de senha","settings.password.heading":"Atualizar senha","settings.password.description":"Certifique-se de que sua conta esteja usando uma senha longa e aleatória para se manter seguro","settings.password.current_password":"Senha atual","settings.password.current_password_placeholder":"Senha atual","settings.password.new_password":"Nova senha","settings.password.new_password_placeholder":"Nova senha","settings.password.confirm_password":"Confirmar senha","settings.password.confirm_password_placeholder":"Confirmar senha","settings.password.save":"Salvar senha","settings.delete_account.heading":"Excluir conta","settings.delete_account.description":"Exclua sua conta e todos os seus recursos","settings.delete_account.warning":"Atenção","settings.delete_account.warning_message":"Por favor, prossiga com cuidado, isso não pode ser desfeito.","settings.delete_account.button":"Excluir conta","settings.delete_account.modal_title":"Tem certeza que deseja excluir sua conta?","settings.delete_account.modal_description":"Uma vez que sua conta for excluída, todos os seus recursos e dados também serão permanentemente excluídos. Por favor, digite sua senha para confirmar que deseja excluir permanentemente sua conta.","settings.delete_account.password":"Senha","settings.delete_account.password_placeholder":"Senha","settings.delete_account.cancel":"Cancelar","settings.delete_account.confirm":"Excluir conta","settings.workspace.title":"Configurações do workspace","settings.workspace.heading":"Configurações do workspace","settings.workspace.description":"Atualize o nome, logo e fuso horário do workspace","settings.workspace.logo":"Logo","settings.workspace.name":"Nome","settings.workspace.name_placeholder":"Meu Workspace","settings.workspace.timezone":"Fuso horário","settings.workspace.save":"Salvar","settings.workspace.saved":"Salvo.","settings.members.title":"Membros","settings.members.heading":"Membros da equipe","settings.members.description":"Gerencie membros e convites deste workspace","settings.members.invite.title":"Convidar Membro","settings.members.invite.description":"Envie um convite por e-mail para adicionar colaboradores","settings.members.invite.email":"E-mail","settings.members.invite.email_placeholder":"colaborador@email.com","settings.members.invite.role":"Função","settings.members.invite.role_placeholder":"Selecione uma função","settings.members.invite.submit":"Enviar Convite","settings.members.invite.cancel_confirm":"Tem certeza que deseja cancelar este convite?","settings.members.pending.title":"Convites Pendentes","settings.members.pending.description":"Convites aguardando aceitação","settings.members.pending.empty":"Nenhum convite pendente","settings.members.list.title":"Membros","settings.members.list.description":"Pessoas com acesso a este workspace","settings.members.list.empty":"Nenhum membro além do proprietário","settings.members.list.remove_confirm":"Tem certeza que deseja remover este membro?","settings.members.roles.owner":"Proprietário","settings.members.roles.admin":"Administrador","settings.members.roles.member":"Membro","settings.members.flash.invite_sent":"Convite enviado com sucesso!","settings.members.flash.invite_deleted":"Convite excluído.","settings.members.flash.member_removed":"Membro removido com sucesso.","settings.members.flash.wrong_email":"Este convite é para um endereço de e-mail diferente.","settings.members.flash.already_member":"Você já é membro deste workspace.","settings.members.flash.invite_accepted":"Bem-vindo! Você agora é membro do workspace.","settings.members.flash.invite_declined":"Convite recusado.","settings.flash.profile_updated":"Perfil atualizado com sucesso!","settings.flash.language_updated":"Idioma atualizado com sucesso!","settings.flash.password_updated":"Senha atualizada com sucesso!","settings.flash.workspace_updated":"Configurações atualizadas com sucesso!","sidebar.select_workspace":"Selecionar workspace","sidebar.create_workspace":"Criar workspace","sidebar.create_post":"Novo post","sidebar.profile":"Perfil","sidebar.log_out":"Sair","sidebar.workspace":"Workspace: :name","sidebar.workspace_select":"Workspace: Selecionar","sidebar.theme":"Tema: :name","sidebar.theme_light":"Claro","sidebar.theme_dark":"Escuro","sidebar.theme_system":"Sistema","sidebar.language":"Idioma: :name","sidebar.language_select":"Idioma: Selecionar","sidebar.groups.posts":"Posts","sidebar.groups.configuration":"Configurações","sidebar.groups.support":"Suporte","sidebar.posts.calendar":"Calendário","sidebar.posts.all":"Todos","sidebar.posts.scheduled":"Agendados","sidebar.posts.posted":"Publicados","sidebar.posts.drafts":"Rascunhos","sidebar.config.connections":"Conexões","sidebar.config.hashtags":"Hashtags","sidebar.config.labels":"Etiquetas","sidebar.config.settings":"Configurações","sidebar.support.discord":"Discord","sidebar.support.share_feedback":"Enviar feedback","sidebar.support.last_updates":"Últimas Atualizações","sidebar.support.docs":"Documentação","validation.accepted":"O campo :attribute deve ser aceito.","validation.accepted_if":"O campo :attribute deve ser aceito quando :other for :value.","validation.active_url":"O campo :attribute deve ser uma URL válida.","validation.after":"O campo :attribute deve ser uma data posterior a :date.","validation.after_or_equal":"O campo :attribute deve ser uma data posterior ou igual a :date.","validation.alpha":"O campo :attribute deve conter apenas letras.","validation.alpha_dash":"O campo :attribute deve conter apenas letras, números, hifens e underscores.","validation.alpha_num":"O campo :attribute deve conter apenas letras e números.","validation.any_of":"O campo :attribute é inválido.","validation.array":"O campo :attribute deve ser um array.","validation.ascii":"O campo :attribute deve conter apenas caracteres alfanuméricos e símbolos de um byte.","validation.before":"O campo :attribute deve ser uma data anterior a :date.","validation.before_or_equal":"O campo :attribute deve ser uma data anterior ou igual a :date.","validation.between.array":"O campo :attribute deve ter entre :min e :max itens.","validation.between.file":"O campo :attribute deve estar entre :min e :max kilobytes.","validation.between.numeric":"O campo :attribute deve estar entre :min e :max.","validation.between.string":"O campo :attribute deve estar entre :min e :max caracteres.","validation.boolean":"O campo :attribute deve ser verdadeiro ou falso.","validation.can":"O campo :attribute contém um valor não autorizado.","validation.confirmed":"A confirmação do campo :attribute não corresponde.","validation.contains":"O campo :attribute está faltando um valor obrigatório.","validation.current_password":"A senha está incorreta.","validation.date":"O campo :attribute deve ser uma data válida.","validation.date_equals":"O campo :attribute deve ser uma data igual a :date.","validation.date_format":"O campo :attribute deve corresponder ao formato :format.","validation.decimal":"O campo :attribute deve ter :decimal casas decimais.","validation.declined":"O campo :attribute deve ser recusado.","validation.declined_if":"O campo :attribute deve ser recusado quando :other for :value.","validation.different":"O campo :attribute e :other devem ser diferentes.","validation.digits":"O campo :attribute deve ter :digits dígitos.","validation.digits_between":"O campo :attribute deve ter entre :min e :max dígitos.","validation.dimensions":"O campo :attribute deve ter dimensões de imagem válidas.","validation.distinct":"O campo :attribute tem um valor duplicado.","validation.doesnt_contain":"O campo :attribute não deve conter nenhum dos seguintes: :values.","validation.doesnt_end_with":"O campo :attribute não deve terminar com nenhum dos seguintes: :values.","validation.doesnt_start_with":"O campo :attribute não deve começar com nenhum dos seguintes: :values.","validation.email":"O campo :attribute deve ser um endereço de e-mail válido.","validation.encoding":"O campo :attribute deve ser codificado em :encoding.","validation.ends_with":"O campo :attribute deve terminar com um dos seguintes: :values.","validation.enum":"O :attribute selecionado é inválido.","validation.exists":"O :attribute selecionado é inválido.","validation.extensions":"O campo :attribute deve ter uma das seguintes extensões: :values.","validation.file":"O campo :attribute deve ser um arquivo.","validation.filled":"O campo :attribute deve ter um valor.","validation.gt.array":"O campo :attribute deve ter mais de :value itens.","validation.gt.file":"O campo :attribute deve ser maior que :value kilobytes.","validation.gt.numeric":"O campo :attribute deve ser maior que :value.","validation.gt.string":"O campo :attribute deve ser maior que :value caracteres.","validation.gte.array":"O campo :attribute deve ter :value itens ou mais.","validation.gte.file":"O campo :attribute deve ser maior ou igual a :value kilobytes.","validation.gte.numeric":"O campo :attribute deve ser maior ou igual a :value.","validation.gte.string":"O campo :attribute deve ser maior ou igual a :value caracteres.","validation.hex_color":"O campo :attribute deve ser uma cor hexadecimal válida.","validation.image":"O campo :attribute deve ser uma imagem.","validation.in":"O :attribute selecionado é inválido.","validation.in_array":"O campo :attribute deve existir em :other.","validation.in_array_keys":"O campo :attribute deve conter pelo menos uma das seguintes chaves: :values.","validation.integer":"O campo :attribute deve ser um inteiro.","validation.ip":"O campo :attribute deve ser um endereço IP válido.","validation.ipv4":"O campo :attribute deve ser um endereço IPv4 válido.","validation.ipv6":"O campo :attribute deve ser um endereço IPv6 válido.","validation.json":"O campo :attribute deve ser uma string JSON válida.","validation.list":"O campo :attribute deve ser uma lista.","validation.lowercase":"O campo :attribute deve estar em minúsculas.","validation.lt.array":"O campo :attribute deve ter menos de :value itens.","validation.lt.file":"O campo :attribute deve ser menor que :value kilobytes.","validation.lt.numeric":"O campo :attribute deve ser menor que :value.","validation.lt.string":"O campo :attribute deve ser menor que :value caracteres.","validation.lte.array":"O campo :attribute deve ter :value itens ou menos.","validation.lte.file":"O campo :attribute deve ser menor ou igual a :value kilobytes.","validation.lte.numeric":"O campo :attribute deve ser menor ou igual a :value.","validation.lte.string":"O campo :attribute deve ser menor ou igual a :value caracteres.","validation.mac_address":"O campo :attribute deve ser um endereço MAC válido.","validation.max.array":"O campo :attribute deve ter no máximo :max itens.","validation.max.file":"O campo :attribute deve ter no máximo :max kilobytes.","validation.max.numeric":"O campo :attribute deve ter no máximo :max.","validation.max.string":"O campo :attribute deve ter no máximo :max caracteres.","validation.max_digits":"O campo :attribute não deve ter mais que :max dígitos.","validation.mimes":"O campo :attribute deve ser um arquivo do tipo: :values.","validation.mimetypes":"O campo :attribute deve ser um arquivo do tipo: :values.","validation.min.array":"O campo :attribute deve ter pelo menos :min itens.","validation.min.file":"O campo :attribute deve ter pelo menos :min kilobytes.","validation.min.numeric":"O campo :attribute deve ter pelo menos :min.","validation.min.string":"O campo :attribute deve ter pelo menos :min caracteres.","validation.min_digits":"O campo :attribute deve ter pelo menos :min dígitos.","validation.missing":"O campo :attribute deve estar ausente.","validation.missing_if":"O campo :attribute deve estar ausente quando :other for :value.","validation.missing_unless":"O campo :attribute deve estar ausente a menos que :other seja :value.","validation.missing_with":"O campo :attribute deve estar ausente quando :values estiver presente.","validation.missing_with_all":"O campo :attribute deve estar ausente quando :values estiverem presentes.","validation.multiple_of":"O campo :attribute deve ser um múltiplo de :value.","validation.not_in":"O :attribute selecionado é inválido.","validation.not_regex":"O formato do campo :attribute é inválido.","validation.numeric":"O campo :attribute deve ser um número.","validation.password.letters":"O campo :attribute deve conter pelo menos uma letra.","validation.password.mixed":"O campo :attribute deve conter pelo menos uma letra maiúscula e uma minúscula.","validation.password.numbers":"O campo :attribute deve conter pelo menos um número.","validation.password.symbols":"O campo :attribute deve conter pelo menos um símbolo.","validation.password.uncompromised":"O :attribute fornecido apareceu em um vazamento de dados. Por favor, escolha um :attribute diferente.","validation.present":"O campo :attribute deve estar presente.","validation.present_if":"O campo :attribute deve estar presente quando :other for :value.","validation.present_unless":"O campo :attribute deve estar presente a menos que :other seja :value.","validation.present_with":"O campo :attribute deve estar presente quando :values estiver presente.","validation.present_with_all":"O campo :attribute deve estar presente quando :values estiverem presentes.","validation.prohibited":"O campo :attribute é proibido.","validation.prohibited_if":"O campo :attribute é proibido quando :other for :value.","validation.prohibited_if_accepted":"O campo :attribute é proibido quando :other for aceito.","validation.prohibited_if_declined":"O campo :attribute é proibido quando :other for recusado.","validation.prohibited_unless":"O campo :attribute é proibido a menos que :other esteja em :values.","validation.prohibits":"O campo :attribute proíbe :other de estar presente.","validation.regex":"O formato do campo :attribute é inválido.","validation.required":"O campo :attribute é obrigatório.","validation.required_array_keys":"O campo :attribute deve conter entradas para: :values.","validation.required_if":"O campo :attribute é obrigatório quando :other for :value.","validation.required_if_accepted":"O campo :attribute é obrigatório quando :other for aceito.","validation.required_if_declined":"O campo :attribute é obrigatório quando :other for recusado.","validation.required_unless":"O campo :attribute é obrigatório a menos que :other esteja em :values.","validation.required_with":"O campo :attribute é obrigatório quando :values estiver presente.","validation.required_with_all":"O campo :attribute é obrigatório quando :values estiverem presentes.","validation.required_without":"O campo :attribute é obrigatório quando :values não estiver presente.","validation.required_without_all":"O campo :attribute é obrigatório quando nenhum dos :values estiver presente.","validation.same":"O campo :attribute deve ser igual a :other.","validation.size.array":"O campo :attribute deve conter :size itens.","validation.size.file":"O campo :attribute deve ter :size kilobytes.","validation.size.numeric":"O campo :attribute deve ser :size.","validation.size.string":"O campo :attribute deve ter :size caracteres.","validation.starts_with":"O campo :attribute deve começar com um dos seguintes: :values.","validation.string":"O campo :attribute deve ser uma string.","validation.timezone":"O campo :attribute deve ser um fuso horário válido.","validation.unique":"O :attribute já foi utilizado.","validation.uploaded":"O :attribute falhou ao ser enviado.","validation.uppercase":"O campo :attribute deve estar em maiúsculo.","validation.url":"O campo :attribute deve ser uma URL válida.","validation.ulid":"O campo :attribute deve ser um ULID válido.","validation.uuid":"O campo :attribute deve ser um UUID válido.","validation.custom.attribute-name.rule-name":"custom-message"} \ No newline at end of file