From b2bf5c2059b44f3a0c6c7dc2c3fd660757a66f12 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 16 Apr 2026 09:25:08 -0300 Subject: [PATCH] feat: complete AI assistant with custom services and brand config Baseline snapshot of custom AI implementation before Laravel AI SDK migration. Includes: - Custom services: GeminiTextGenerationService, TextGenerationService (OpenAI), ImageGenerationService, AudioGenerationService, VideoGenerationService - IntentDetector for content moderation via keyword matching - AI enums: Intent, Orientation, UsageType - Blade prompt templates: system.blade.php, image.blade.php, video.blade.php - AiMessage with content_html accessor (markdown rendering) - AiUsageLog for monthly quota tracking per account - PostAssistantController with regex-based [GENERATE_*] parsing - WritingAssistantTab with markdown rendering, add-to-post, attachments - Workspace brand fields (name, description, tone, voice_notes) in system prompt - Session state block injected into prompts (thread counts, quota remaining) - AttachmentCollector pattern will replace the regex approach in Phase 2 - Post comments with replies, emoji reactions, real-time via Echo - Assets page with Unsplash + Giphy integrations --- .env.example | 17 ++ app/Broadcasting/PostChannel.php | 20 +- app/Enums/Ai/Intent.php | 14 + app/Enums/Ai/Orientation.php | 19 ++ app/Enums/Ai/UsageType.php | 12 + app/Events/PostCommentCreated.php | 2 +- app/Events/PostPlatformStatusUpdated.php | 2 +- app/Http/Controllers/App/AssetController.php | 16 +- app/Http/Controllers/App/GiphyController.php | 12 +- .../App/PostAssistantController.php | 194 ++++++++++---- .../Controllers/App/PostCommentController.php | 55 ++-- .../Controllers/App/UnsplashController.php | 12 +- .../Middleware/App/HandleInertiaRequests.php | 1 + app/Http/Requests/App/Asset/SearchRequest.php | 23 ++ .../App/Asset/StoreAssetFromUrlRequest.php | 24 ++ .../Requests/App/Asset/StoreAssetRequest.php | 22 ++ .../StoreAssistantMessageRequest.php | 23 ++ .../PostComment/ReactPostCommentRequest.php | 22 ++ .../PostComment/StorePostCommentRequest.php | 23 ++ .../PostComment/UpdatePostCommentRequest.php | 22 ++ app/Models/AiMessage.php | 15 ++ app/Models/AiUsageLog.php | 11 +- app/Providers/AppServiceProvider.php | 11 + app/Services/Ai/AudioGenerationService.php | 77 +++--- .../Ai/Contracts/TextGenerationInterface.php | 15 ++ .../Ai/GeminiTextGenerationService.php | 86 ++++++ app/Services/Ai/ImageGenerationService.php | 138 ++++++---- app/Services/Ai/IntentDetector.php | 37 ++- app/Services/Ai/TextGenerationService.php | 36 ++- app/Services/Ai/VideoGenerationService.php | 144 ++++++---- app/Services/GiphyService.php | 8 + app/Services/UnsplashService.php | 8 + composer.json | 1 - composer.lock | 149 +---------- config/ai.php | 132 --------- config/services.php | 8 + config/trypost.php | 17 ++ database/factories/AiUsageLogFactory.php | 9 +- ...32520_create_agent_conversations_table.php | 52 ---- lang/en/assistant.php | 1 + lang/php_en.json | 1 - lang/php_es.json | 1 - lang/php_pt-br.json | 1 - resources/css/app.css | 1 + .../posts/editor/WritingAssistantTab.vue | 253 ++++++++++++++---- .../views/prompts/assistant/image.blade.php | 19 +- .../views/prompts/assistant/system.blade.php | 147 +++++++++- .../views/prompts/assistant/video.blade.php | 18 ++ routes/channels.php | 10 +- tests/Feature/AiUsageLogTest.php | 44 ++- tests/Feature/PostAssistantTest.php | 108 +++++++- .../Events/PostPlatformStatusUpdatedTest.php | 2 +- 52 files changed, 1389 insertions(+), 706 deletions(-) create mode 100644 app/Enums/Ai/Intent.php create mode 100644 app/Enums/Ai/Orientation.php create mode 100644 app/Enums/Ai/UsageType.php create mode 100644 app/Http/Requests/App/Asset/SearchRequest.php create mode 100644 app/Http/Requests/App/Asset/StoreAssetFromUrlRequest.php create mode 100644 app/Http/Requests/App/Asset/StoreAssetRequest.php create mode 100644 app/Http/Requests/App/Assistant/StoreAssistantMessageRequest.php create mode 100644 app/Http/Requests/App/PostComment/ReactPostCommentRequest.php create mode 100644 app/Http/Requests/App/PostComment/StorePostCommentRequest.php create mode 100644 app/Http/Requests/App/PostComment/UpdatePostCommentRequest.php create mode 100644 app/Services/Ai/Contracts/TextGenerationInterface.php create mode 100644 app/Services/Ai/GeminiTextGenerationService.php delete mode 100644 config/ai.php delete mode 100644 database/migrations/2026_03_29_232520_create_agent_conversations_table.php delete mode 100644 lang/php_en.json delete mode 100644 lang/php_es.json delete mode 100644 lang/php_pt-br.json create mode 100644 resources/views/prompts/assistant/video.blade.php diff --git a/.env.example b/.env.example index 572d5646..a680d0dd 100644 --- a/.env.example +++ b/.env.example @@ -134,6 +134,23 @@ PINTEREST_CLIENT_ID= PINTEREST_CLIENT_SECRET= PINTEREST_CLIENT_REDIRECT="${APP_URL}/accounts/pinterest/callback" +# AI Services +OPENAI_API_KEY= +GEMINI_API_KEY= +ELEVENLABS_API_KEY= +ELEVENLABS_DEFAULT_VOICE=EXAVITQu4vr4xnSDxMaL + +# AI Provider Selection (text: gemini/openai, image: gemini, audio: elevenlabs, video: gemini) +AI_TEXT_PROVIDER=gemini +AI_IMAGE_PROVIDER=gemini +AI_AUDIO_PROVIDER=elevenlabs +AI_VIDEO_PROVIDER=gemini + +# Media Services +UNSPLASH_ACCESS_KEY= +UNSPLASH_SECRET_KEY= +GIPHY_API_KEY= + # Google Tag Manager (optional - analytics) GTM_ID= diff --git a/app/Broadcasting/PostChannel.php b/app/Broadcasting/PostChannel.php index b653acc2..93a7c43c 100644 --- a/app/Broadcasting/PostChannel.php +++ b/app/Broadcasting/PostChannel.php @@ -6,27 +6,11 @@ use App\Models\Post; use App\Models\User; -use Illuminate\Support\Facades\Log; class PostChannel { - public function join(User $user, Post $post): array|bool + public function join(User $user, Post $post): bool { - Log::info('PostChannel::join called', [ - 'user_id' => $user->id, - 'user_email' => $user->email, - 'post_id' => $post->id, - 'workspace_id' => $post->workspace_id, - ]); - - $result = $post->workspace->hasMember($user); - - Log::info('PostChannel::join result', [ - 'result' => $result, - 'workspace_owner_id' => $post->workspace->user_id, - 'is_owner' => $post->workspace->user_id === $user->id, - ]); - - return $result; + return $post->workspace->hasMember($user); } } diff --git a/app/Enums/Ai/Intent.php b/app/Enums/Ai/Intent.php new file mode 100644 index 00000000..f5b78306 --- /dev/null +++ b/app/Enums/Ai/Intent.php @@ -0,0 +1,14 @@ + '9:16', + self::Horizontal => '16:9', + }; + } +} diff --git a/app/Enums/Ai/UsageType.php b/app/Enums/Ai/UsageType.php new file mode 100644 index 00000000..3378184b --- /dev/null +++ b/app/Enums/Ai/UsageType.php @@ -0,0 +1,12 @@ + $this->comment->body, 'reactions' => $this->comment->reactions ?? [], 'created_at' => $this->comment->created_at->toISOString(), - 'updated_at' => $this->comment->created_at->toISOString(), + 'updated_at' => $this->comment->updated_at->toISOString(), 'user' => [ 'id' => $this->comment->user->id, 'name' => $this->comment->user->name, diff --git a/app/Events/PostPlatformStatusUpdated.php b/app/Events/PostPlatformStatusUpdated.php index c2bea369..9634d784 100644 --- a/app/Events/PostPlatformStatusUpdated.php +++ b/app/Events/PostPlatformStatusUpdated.php @@ -20,7 +20,7 @@ public function __construct(public PostPlatform $postPlatform) {} public function broadcastOn(): array { return [ - new PrivateChannel('posts.'.$this->postPlatform->post_id), + new PrivateChannel('post.'.$this->postPlatform->post_id), ]; } diff --git a/app/Http/Controllers/App/AssetController.php b/app/Http/Controllers/App/AssetController.php index aa1f1265..06a6a76d 100644 --- a/app/Http/Controllers/App/AssetController.php +++ b/app/Http/Controllers/App/AssetController.php @@ -4,6 +4,8 @@ namespace App\Http\Controllers\App; +use App\Http\Requests\App\Asset\StoreAssetFromUrlRequest; +use App\Http\Requests\App\Asset\StoreAssetRequest; use App\Models\Media; use App\Services\UnsplashService; use Illuminate\Http\JsonResponse; @@ -37,16 +39,12 @@ public function index(Request $request): Response|RedirectResponse ]); } - public function store(Request $request): JsonResponse + public function store(StoreAssetRequest $request): JsonResponse { $workspace = $request->user()->currentWorkspace; $this->authorize('createPost', $workspace); - $request->validate([ - 'media' => ['required', 'file', 'max:1048576', 'mimetypes:image/jpeg,image/png,image/gif,image/webp,video/mp4'], // max 1GB in KB - ]); - $media = $workspace->addMedia($request->file('media'), 'assets'); return response()->json([ @@ -126,17 +124,13 @@ public function storeChunked(Request $request): JsonResponse ]); } - public function storeFromUrl(Request $request, UnsplashService $unsplash): RedirectResponse + public function storeFromUrl(StoreAssetFromUrlRequest $request, UnsplashService $unsplash): RedirectResponse { $workspace = $request->user()->currentWorkspace; $this->authorize('createPost', $workspace); - $validated = $request->validate([ - 'url' => ['required', 'url', 'regex:/^https:\/\/(images\.unsplash\.com|media[0-9]*\.giphy\.com)\//'], - 'filename' => ['required', 'string', 'max:255'], - 'download_location' => ['nullable', 'url', 'regex:/^https:\/\/api\.unsplash\.com\//'], - ]); + $validated = $request->validated(); // Trigger Unsplash download tracking (required by API guidelines) if ($downloadLocation = data_get($validated, 'download_location')) { diff --git a/app/Http/Controllers/App/GiphyController.php b/app/Http/Controllers/App/GiphyController.php index 19087bcd..53301c90 100644 --- a/app/Http/Controllers/App/GiphyController.php +++ b/app/Http/Controllers/App/GiphyController.php @@ -4,26 +4,24 @@ namespace App\Http\Controllers\App; +use App\Http\Requests\App\Asset\SearchRequest; use App\Services\GiphyService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; class GiphyController extends Controller { - public function search(Request $request, GiphyService $giphy): JsonResponse + public function search(SearchRequest $request, GiphyService $giphy): JsonResponse { $workspace = $request->user()->currentWorkspace; $this->authorize('createPost', $workspace); - $request->validate([ - 'query' => ['required', 'string', 'max:255'], - 'page' => ['sometimes', 'integer', 'min:1'], - ]); + $validated = $request->validated(); $results = $giphy->search( - query: $request->input('query'), - page: $request->integer('page', 1), + query: data_get($validated, 'query'), + page: (int) data_get($validated, 'page', 1), ); return response()->json($results); diff --git a/app/Http/Controllers/App/PostAssistantController.php b/app/Http/Controllers/App/PostAssistantController.php index e0269bdb..aa3b0599 100644 --- a/app/Http/Controllers/App/PostAssistantController.php +++ b/app/Http/Controllers/App/PostAssistantController.php @@ -4,15 +4,19 @@ namespace App\Http\Controllers\App; +use App\Enums\Ai\Intent; +use App\Enums\Ai\Orientation; +use App\Enums\Ai\UsageType; use App\Features\AiImagesLimit; use App\Features\AiVideosLimit; +use App\Http\Requests\App\Assistant\StoreAssistantMessageRequest; use App\Models\AiMessage; use App\Models\AiUsageLog; use App\Models\Post; use App\Services\Ai\AudioGenerationService; +use App\Services\Ai\Contracts\TextGenerationInterface; use App\Services\Ai\ImageGenerationService; use App\Services\Ai\IntentDetector; -use App\Services\Ai\TextGenerationService; use App\Services\Ai\VideoGenerationService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -39,10 +43,10 @@ public function index(Request $request, Post $post): JsonResponse } public function store( - Request $request, + StoreAssistantMessageRequest $request, Post $post, IntentDetector $intentDetector, - TextGenerationService $textService, + TextGenerationInterface $textService, ImageGenerationService $imageService, AudioGenerationService $audioService, VideoGenerationService $videoService, @@ -53,9 +57,7 @@ public function store( abort(Response::HTTP_FORBIDDEN); } - $validated = $request->validate([ - 'body' => ['required', 'string', 'max:2000'], - ]); + $validated = $request->validated(); $prompt = data_get($validated, 'body'); @@ -65,74 +67,160 @@ public function store( 'content' => $prompt, ]); + $imageUrl = null; + if ($request->hasFile('image')) { + $media = $workspace->addMedia($request->file('image'), 'assets'); + $imageUrl = $media->url; + + $userMessage->update([ + 'attachments' => [['id' => $media->id, 'path' => $media->path, 'url' => $media->url, 'type' => 'image', 'mime_type' => $media->mime_type]], + ]); + } + $userMessage->load('user'); $intent = $intentDetector->detect($prompt); + if ($intent === Intent::Blocked) { + $assistantMessage = $post->aiMessages()->create([ + 'role' => 'assistant', + 'content' => __('assistant.content_blocked'), + 'metadata' => ['intent' => $intent->value, 'error' => true], + ]); + + return response()->json([ + 'user_message' => $userMessage, + 'assistant_message' => $assistantMessage, + ], Response::HTTP_CREATED); + } + try { - if ($intent === 'image') { - $limit = Feature::for($workspace->account)->value(AiImagesLimit::class); - $used = AiUsageLog::monthlyCount($workspace->account_id, 'image'); + $previousMessages = $post->aiMessages() + ->whereIn('role', ['user', 'assistant']) + ->where('id', '!=', $userMessage->id) + ->oldest() + ->limit(20) + ->get(); - if ($used >= $limit) { - $assistantMessage = $post->aiMessages()->create([ - 'role' => 'assistant', - 'content' => __('assistant.limit_reached_images'), - 'metadata' => ['intent' => $intent, 'limit_reached' => true], - ]); + $imagesInThread = 0; + $videosInThread = 0; - return response()->json(['user_message' => $userMessage, 'assistant_message' => $assistantMessage], Response::HTTP_CREATED); - } - } + $history = $previousMessages + ->map(function (AiMessage $m) use (&$imagesInThread, &$videosInThread) { + $content = $m->content; - if ($intent === 'video') { - $limit = Feature::for($workspace->account)->value(AiVideosLimit::class); - $used = AiUsageLog::monthlyCount($workspace->account_id, 'video'); + if ($m->role === 'assistant' && ! empty($m->attachments)) { + $attachmentTypes = collect($m->attachments) + ->groupBy('type') + ->map(fn ($group) => count($group)); - if ($used >= $limit) { - $assistantMessage = $post->aiMessages()->create([ - 'role' => 'assistant', - 'content' => __('assistant.limit_reached_videos'), - 'metadata' => ['intent' => $intent, 'limit_reached' => true], - ]); + $counts = []; + foreach ($attachmentTypes as $type => $count) { + $counts[] = "{$count} {$type}"; + if ($type === 'image') { + $imagesInThread += $count; + } elseif ($type === 'video') { + $videosInThread += $count; + } + } - return response()->json(['user_message' => $userMessage, 'assistant_message' => $assistantMessage], Response::HTTP_CREATED); - } - } + $content .= "\n\n[This assistant message attached: ".implode(', ', $counts).']'; + } + + return ['role' => $m->role, 'content' => $content]; + }) + ->all(); + + $imageLimit = (int) Feature::for($workspace->account)->value(AiImagesLimit::class); + $imageUsed = AiUsageLog::monthlyCount($workspace->account_id, UsageType::Image); + $imageRemaining = max(0, $imageLimit - $imageUsed); + + $videoLimit = (int) Feature::for($workspace->account)->value(AiVideosLimit::class); + $videoUsed = AiUsageLog::monthlyCount($workspace->account_id, UsageType::Video); + $videoRemaining = max(0, $videoLimit - $videoUsed); + + $stateContext = sprintf( + "[Session state — use this to track progress and respect quotas]\n". + "- Images already generated in this conversation: %d\n". + "- Videos already generated in this conversation: %d\n". + "- Monthly quota remaining: %d images, %d videos\n", + $imagesInThread, + $videosInThread, + $imageRemaining, + $videoRemaining, + ); + + $promptWithState = "{$stateContext}\n{$prompt}"; + + $responseContent = $textService->generate($promptWithState, $history, $workspace, $imageUrl); - $responseContent = ''; $attachments = []; + $generatedIntent = $intent->value; + $limitReached = false; - if ($intent === 'image') { - $result = $imageService->generate($prompt, $workspace, $request->user()->id, $post->id); - $responseContent = __('assistant.image_generated'); - $attachments = [$result]; - } elseif ($intent === 'video') { - $result = $videoService->generate($prompt, $workspace, $request->user()->id, $post->id); - $responseContent = __('assistant.video_generated'); - $attachments = [$result]; - } elseif ($intent === 'audio') { + // Build rich context from conversation history for media generation + $conversationContext = collect($history) + ->map(fn (array $m) => "{$m['role']}: {$m['content']}") + ->implode("\n\n"); + + $buildMediaPrompt = function (string $captionText) use ($prompt, $conversationContext): string { + $parts = [$prompt]; + + if ($conversationContext) { + $parts[] = "Conversation context:\n{$conversationContext}"; + } + + if ($captionText) { + $parts[] = "Caption generated for this post:\n{$captionText}"; + } + + return implode("\n\n", $parts); + }; + + if (preg_match('/\[GENERATE_IMAGE:(vertical|horizontal)\]/', $responseContent, $matches)) { + $orientation = Orientation::tryFrom(data_get($matches, 1, 'vertical')) ?? Orientation::Vertical; + $limit = Feature::for($workspace->account)->value(AiImagesLimit::class); + $used = AiUsageLog::monthlyCount($workspace->account_id, UsageType::Image); + + if ($used >= $limit) { + $responseContent = __('assistant.limit_reached_images'); + $limitReached = true; + } else { + $captionText = trim(preg_replace('/\[GENERATE_IMAGE:(vertical|horizontal)\]/', '', $responseContent)); + $mediaPrompt = $buildMediaPrompt($captionText); + $result = $imageService->generate($mediaPrompt, $workspace, $request->user()->id, $post->id, $orientation); + $responseContent = $captionText ?: __('assistant.image_generated'); + $attachments = [$result]; + $generatedIntent = 'image'; + } + } elseif (preg_match('/\[GENERATE_VIDEO:(vertical|horizontal)\]/', $responseContent, $matches)) { + $orientation = Orientation::tryFrom(data_get($matches, 1, 'vertical')) ?? Orientation::Vertical; + $limit = Feature::for($workspace->account)->value(AiVideosLimit::class); + $used = AiUsageLog::monthlyCount($workspace->account_id, UsageType::Video); + + if ($used >= $limit) { + $responseContent = __('assistant.limit_reached_videos'); + $limitReached = true; + } else { + $captionText = trim(preg_replace('/\[GENERATE_VIDEO:(vertical|horizontal)\]/', '', $responseContent)); + $mediaPrompt = $buildMediaPrompt($captionText); + $result = $videoService->generate($mediaPrompt, $workspace, $request->user()->id, $post->id, $orientation); + $responseContent = $captionText ?: __('assistant.video_generated'); + $attachments = [$result]; + $generatedIntent = 'video'; + } + } elseif (str_contains($responseContent, '[GENERATE_AUDIO]')) { $result = $audioService->generate($prompt, $workspace, $request->user()->id, $post->id); $responseContent = __('assistant.audio_generated'); $attachments = [$result]; - } else { - $history = $post->aiMessages() - ->whereIn('role', ['user', 'assistant']) - ->where('id', '!=', $userMessage->id) - ->oldest() - ->limit(20) - ->get() - ->map(fn (AiMessage $m) => ['role' => $m->role, 'content' => $m->content]) - ->all(); - - $responseContent = $textService->generate($prompt, $history, $workspace); + $generatedIntent = 'audio'; } $assistantMessage = $post->aiMessages()->create([ 'role' => 'assistant', 'content' => $responseContent, 'attachments' => $attachments, - 'metadata' => ['intent' => $intent], + 'metadata' => array_filter(['intent' => $generatedIntent, 'limit_reached' => $limitReached ?: null]), ]); return response()->json([ @@ -147,7 +235,7 @@ public function store( $assistantMessage = $post->aiMessages()->create([ 'role' => 'assistant', 'content' => $errorMessage, - 'metadata' => ['intent' => $intent, 'error' => true], + 'metadata' => ['intent' => $intent->value, 'error' => true], ]); return response()->json([ diff --git a/app/Http/Controllers/App/PostCommentController.php b/app/Http/Controllers/App/PostCommentController.php index 1ae35a72..fc522fb1 100644 --- a/app/Http/Controllers/App/PostCommentController.php +++ b/app/Http/Controllers/App/PostCommentController.php @@ -5,6 +5,9 @@ namespace App\Http\Controllers\App; use App\Events\PostCommentCreated; +use App\Http\Requests\App\PostComment\ReactPostCommentRequest; +use App\Http\Requests\App\PostComment\StorePostCommentRequest; +use App\Http\Requests\App\PostComment\UpdatePostCommentRequest; use App\Models\Post; use App\Models\PostComment; use Illuminate\Http\JsonResponse; @@ -30,7 +33,7 @@ public function index(Request $request, Post $post): JsonResponse return response()->json($comments); } - public function store(Request $request, Post $post): JsonResponse + public function store(StorePostCommentRequest $request, Post $post): JsonResponse { $workspace = $request->user()->currentWorkspace; @@ -38,14 +41,18 @@ public function store(Request $request, Post $post): JsonResponse abort(Response::HTTP_FORBIDDEN); } - $validated = $request->validate([ - 'body' => ['required', 'string', 'max:2000'], - 'parent_id' => ['nullable', 'uuid', 'exists:post_comments,id'], - ]); + $validated = $request->validated(); if (data_get($validated, 'parent_id')) { - $parent = PostComment::find(data_get($validated, 'parent_id')); - if ($parent && $parent->parent_id !== null) { + $parent = PostComment::where('id', data_get($validated, 'parent_id')) + ->where('post_id', $post->id) + ->first(); + + if (! $parent) { + abort(Response::HTTP_NOT_FOUND); + } + + if ($parent->parent_id !== null) { abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'Cannot reply to a reply.'); } } @@ -63,15 +70,22 @@ public function store(Request $request, Post $post): JsonResponse return response()->json($comment, Response::HTTP_CREATED); } - public function update(Request $request, Post $post, PostComment $comment): JsonResponse + public function update(UpdatePostCommentRequest $request, Post $post, PostComment $comment): JsonResponse { + if ($comment->post_id !== $post->id) { + abort(Response::HTTP_NOT_FOUND); + } + if ($comment->user_id !== $request->user()->id) { abort(Response::HTTP_FORBIDDEN); } - $validated = $request->validate([ - 'body' => ['required', 'string', 'max:2000'], - ]); + $workspace = $request->user()->currentWorkspace; + if ($comment->post->workspace_id !== $workspace->id) { + abort(Response::HTTP_FORBIDDEN); + } + + $validated = $request->validated(); $comment->update(['body' => data_get($validated, 'body')]); @@ -80,26 +94,37 @@ public function update(Request $request, Post $post, PostComment $comment): Json public function destroy(Request $request, Post $post, PostComment $comment): JsonResponse { + if ($comment->post_id !== $post->id) { + abort(Response::HTTP_NOT_FOUND); + } + if ($comment->user_id !== $request->user()->id) { abort(Response::HTTP_FORBIDDEN); } + $workspace = $request->user()->currentWorkspace; + if ($comment->post->workspace_id !== $workspace->id) { + abort(Response::HTTP_FORBIDDEN); + } + $comment->delete(); return response()->json(null, Response::HTTP_NO_CONTENT); } - public function react(Request $request, Post $post, PostComment $comment): JsonResponse + public function react(ReactPostCommentRequest $request, Post $post, PostComment $comment): JsonResponse { + if ($comment->post_id !== $post->id) { + abort(Response::HTTP_NOT_FOUND); + } + $workspace = $request->user()->currentWorkspace; if ($post->workspace_id !== $workspace->id) { abort(Response::HTTP_FORBIDDEN); } - $validated = $request->validate([ - 'emoji' => ['required', 'string', 'max:10'], - ]); + $validated = $request->validated(); $comment->addReaction($request->user()->id, data_get($validated, 'emoji')); diff --git a/app/Http/Controllers/App/UnsplashController.php b/app/Http/Controllers/App/UnsplashController.php index 7b6a6f0b..4cba3e4a 100644 --- a/app/Http/Controllers/App/UnsplashController.php +++ b/app/Http/Controllers/App/UnsplashController.php @@ -4,26 +4,24 @@ namespace App\Http\Controllers\App; +use App\Http\Requests\App\Asset\SearchRequest; use App\Services\UnsplashService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; class UnsplashController extends Controller { - public function search(Request $request, UnsplashService $unsplash): JsonResponse + public function search(SearchRequest $request, UnsplashService $unsplash): JsonResponse { $workspace = $request->user()->currentWorkspace; $this->authorize('createPost', $workspace); - $request->validate([ - 'query' => ['required', 'string', 'max:255'], - 'page' => ['sometimes', 'integer', 'min:1'], - ]); + $validated = $request->validated(); $results = $unsplash->search( - query: $request->input('query'), - page: $request->integer('page', 1), + query: data_get($validated, 'query'), + page: (int) data_get($validated, 'page', 1), ); return response()->json($results); diff --git a/app/Http/Middleware/App/HandleInertiaRequests.php b/app/Http/Middleware/App/HandleInertiaRequests.php index 6deda69c..fa5362d2 100644 --- a/app/Http/Middleware/App/HandleInertiaRequests.php +++ b/app/Http/Middleware/App/HandleInertiaRequests.php @@ -46,6 +46,7 @@ public function share(Request $request): array 'code' => $code, 'name' => $name, ])->values()->all(), + 'aiEnabled' => ! empty(config('services.gemini.api_key')) || ! empty(config('services.openai.api_key')), 'selfHosted' => config('trypost.self_hosted'), 'googleAuthEnabled' => config('trypost.google_auth_enabled'), ]; diff --git a/app/Http/Requests/App/Asset/SearchRequest.php b/app/Http/Requests/App/Asset/SearchRequest.php new file mode 100644 index 00000000..ddb90f78 --- /dev/null +++ b/app/Http/Requests/App/Asset/SearchRequest.php @@ -0,0 +1,23 @@ + ['required', 'string', 'max:255'], + 'page' => ['sometimes', 'integer', 'min:1'], + ]; + } +} diff --git a/app/Http/Requests/App/Asset/StoreAssetFromUrlRequest.php b/app/Http/Requests/App/Asset/StoreAssetFromUrlRequest.php new file mode 100644 index 00000000..7af43adb --- /dev/null +++ b/app/Http/Requests/App/Asset/StoreAssetFromUrlRequest.php @@ -0,0 +1,24 @@ + ['required', 'url', 'regex:/^https:\/\/(images\.unsplash\.com|media[0-9]*\.giphy\.com)\//'], + 'filename' => ['required', 'string', 'max:255'], + 'download_location' => ['nullable', 'url', 'regex:/^https:\/\/api\.unsplash\.com\//'], + ]; + } +} diff --git a/app/Http/Requests/App/Asset/StoreAssetRequest.php b/app/Http/Requests/App/Asset/StoreAssetRequest.php new file mode 100644 index 00000000..96cf2b21 --- /dev/null +++ b/app/Http/Requests/App/Asset/StoreAssetRequest.php @@ -0,0 +1,22 @@ + ['required', 'file', 'max:1048576', 'mimetypes:image/jpeg,image/png,image/gif,image/webp,video/mp4'], + ]; + } +} diff --git a/app/Http/Requests/App/Assistant/StoreAssistantMessageRequest.php b/app/Http/Requests/App/Assistant/StoreAssistantMessageRequest.php new file mode 100644 index 00000000..264049ed --- /dev/null +++ b/app/Http/Requests/App/Assistant/StoreAssistantMessageRequest.php @@ -0,0 +1,23 @@ + ['required', 'string', 'max:2000'], + 'image' => ['nullable', 'file', 'max:10240', 'mimetypes:image/jpeg,image/png,image/gif,image/webp'], + ]; + } +} diff --git a/app/Http/Requests/App/PostComment/ReactPostCommentRequest.php b/app/Http/Requests/App/PostComment/ReactPostCommentRequest.php new file mode 100644 index 00000000..3fa68118 --- /dev/null +++ b/app/Http/Requests/App/PostComment/ReactPostCommentRequest.php @@ -0,0 +1,22 @@ + ['required', 'string', 'max:10'], + ]; + } +} diff --git a/app/Http/Requests/App/PostComment/StorePostCommentRequest.php b/app/Http/Requests/App/PostComment/StorePostCommentRequest.php new file mode 100644 index 00000000..0353edc8 --- /dev/null +++ b/app/Http/Requests/App/PostComment/StorePostCommentRequest.php @@ -0,0 +1,23 @@ + ['required', 'string', 'max:2000'], + 'parent_id' => ['nullable', 'uuid', 'exists:post_comments,id'], + ]; + } +} diff --git a/app/Http/Requests/App/PostComment/UpdatePostCommentRequest.php b/app/Http/Requests/App/PostComment/UpdatePostCommentRequest.php new file mode 100644 index 00000000..e06dc733 --- /dev/null +++ b/app/Http/Requests/App/PostComment/UpdatePostCommentRequest.php @@ -0,0 +1,22 @@ + ['required', 'string', 'max:2000'], + ]; + } +} diff --git a/app/Models/AiMessage.php b/app/Models/AiMessage.php index 26780b07..ed0bb4c5 100644 --- a/app/Models/AiMessage.php +++ b/app/Models/AiMessage.php @@ -5,10 +5,12 @@ namespace App\Models; use Database\Factories\AiMessageFactory; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Str; class AiMessage extends Model { @@ -32,6 +34,19 @@ protected function casts(): array ]; } + /** + * @return array + */ + protected $appends = ['content_html']; + + protected function contentHtml(): Attribute + { + return Attribute::get(fn () => $this->role === 'assistant' && $this->content + ? Str::markdown($this->content) + : null + ); + } + public function post(): BelongsTo { return $this->belongsTo(Post::class); diff --git a/app/Models/AiUsageLog.php b/app/Models/AiUsageLog.php index 45d60b2e..08a6ec7d 100644 --- a/app/Models/AiUsageLog.php +++ b/app/Models/AiUsageLog.php @@ -4,6 +4,7 @@ namespace App\Models; +use App\Enums\Ai\UsageType; use Database\Factories\AiUsageLogFactory; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -29,7 +30,10 @@ class AiUsageLog extends Model protected function casts(): array { - return ['metadata' => 'array']; + return [ + 'type' => UsageType::class, + 'metadata' => 'array', + ]; } public function account(): BelongsTo @@ -37,12 +41,11 @@ public function account(): BelongsTo return $this->belongsTo(Account::class); } - public static function monthlyCount(string $accountId, string $type): int + public static function monthlyCount(string $accountId, UsageType $type): int { return static::where('account_id', $accountId) ->where('type', $type) - ->whereMonth('created_at', now()->month) - ->whereYear('created_at', now()->year) + ->whereBetween('created_at', [now()->startOfMonth(), now()->endOfMonth()]) ->count(); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 804fbbd6..00e14fb2 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -23,6 +23,9 @@ use App\Models\Workspace; use App\Models\WorkspaceHashtag; use App\Models\WorkspaceLabel; +use App\Services\Ai\Contracts\TextGenerationInterface; +use App\Services\Ai\GeminiTextGenerationService; +use App\Services\Ai\TextGenerationService; use App\Socialite\InstagramProvider; use App\Socialite\LinkedInPageExtendSocialite; use Carbon\CarbonImmutable; @@ -65,6 +68,14 @@ public function register(): void $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class); $this->app->register(TelescopeServiceProvider::class); } + + $this->app->bind( + TextGenerationInterface::class, + fn () => match (config('trypost.ai.text_provider')) { + 'openai' => new TextGenerationService, + default => new GeminiTextGenerationService, + }, + ); } /** diff --git a/app/Services/Ai/AudioGenerationService.php b/app/Services/Ai/AudioGenerationService.php index 26b87167..c7caa7a2 100644 --- a/app/Services/Ai/AudioGenerationService.php +++ b/app/Services/Ai/AudioGenerationService.php @@ -4,8 +4,10 @@ namespace App\Services\Ai; +use App\Enums\Ai\UsageType; use App\Models\AiUsageLog; use App\Models\Workspace; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; @@ -13,13 +15,13 @@ class AudioGenerationService { - private string $apiKey; - private string $baseUrl = 'https://api.elevenlabs.io/v1'; + private string $apiKey; + public function __construct() { - $this->apiKey = config('services.elevenlabs.api_key', ''); + $this->apiKey = config('services.elevenlabs.api_key') ?? ''; } /** @@ -27,12 +29,15 @@ public function __construct() */ public function generate(string $text, Workspace $workspace, ?string $userId = null, ?string $postId = null, ?string $voiceId = null): array { - $voiceId ??= config('services.elevenlabs.default_voice', 'EXAVITQu4vr4xnSDxMaL'); + if (empty($this->apiKey)) { + throw new \RuntimeException('ElevenLabs API key is not configured. Please set ELEVENLABS_API_KEY in your .env file.'); + } + + $voiceId ??= config('services.elevenlabs.default_voice') ?? 'EXAVITQu4vr4xnSDxMaL'; $response = Http::timeout(120) ->withHeaders([ 'xi-api-key' => $this->apiKey, - 'Content-Type' => 'application/json', 'Accept' => 'audio/mpeg', ]) ->post("{$this->baseUrl}/text-to-speech/{$voiceId}", [ @@ -50,38 +55,42 @@ public function generate(string $text, Workspace $workspace, ?string $userId = n throw new \RuntimeException('Failed to generate audio. Please try again.'); } - $filename = Str::uuid().'.mp3'; - $path = 'medias/'.$filename; + $audioContent = $response->body(); - Storage::put($path, $response->body()); + return DB::transaction(function () use ($audioContent, $text, $workspace, $userId, $postId) { + $filename = Str::uuid().'.mp3'; + $path = 'medias/'.$filename; - $media = $workspace->media()->create([ - 'group_id' => Str::uuid()->toString(), - 'collection' => 'assets', - 'type' => 'video', - 'path' => $path, - 'original_filename' => 'ai-generated.mp3', - 'mime_type' => 'audio/mpeg', - 'size' => strlen($response->body()), - 'order' => 0, - 'meta' => ['ai_generated' => true, 'text' => Str::limit($text, 200)], - ]); + Storage::put($path, $audioContent); - AiUsageLog::create([ - 'account_id' => $workspace->account_id, - 'workspace_id' => $workspace->id, - 'user_id' => $userId, - 'post_id' => $postId, - 'type' => 'audio', - 'provider' => 'elevenlabs', - ]); + $media = $workspace->media()->create([ + 'group_id' => Str::uuid()->toString(), + 'collection' => 'assets', + 'type' => 'video', + 'path' => $path, + 'original_filename' => 'ai-generated.mp3', + 'mime_type' => 'audio/mpeg', + 'size' => strlen($audioContent), + 'order' => 0, + 'meta' => ['ai_generated' => true, 'text' => Str::limit($text, 200)], + ]); - return [ - 'id' => $media->id, - 'path' => $media->path, - 'url' => $media->url, - 'mime_type' => 'audio/mpeg', - 'type' => 'audio', - ]; + AiUsageLog::create([ + 'account_id' => $workspace->account_id, + 'workspace_id' => $workspace->id, + 'user_id' => $userId, + 'post_id' => $postId, + 'type' => UsageType::Audio, + 'provider' => 'elevenlabs', + ]); + + return [ + 'id' => $media->id, + 'path' => $media->path, + 'url' => $media->url, + 'mime_type' => 'audio/mpeg', + 'type' => 'audio', + ]; + }); } } diff --git a/app/Services/Ai/Contracts/TextGenerationInterface.php b/app/Services/Ai/Contracts/TextGenerationInterface.php new file mode 100644 index 00000000..0daf52ef --- /dev/null +++ b/app/Services/Ai/Contracts/TextGenerationInterface.php @@ -0,0 +1,15 @@ + $history + */ + public function generate(string $prompt, array $history = [], ?Workspace $workspace = null, ?string $imageUrl = null): string; +} diff --git a/app/Services/Ai/GeminiTextGenerationService.php b/app/Services/Ai/GeminiTextGenerationService.php new file mode 100644 index 00000000..0277f062 --- /dev/null +++ b/app/Services/Ai/GeminiTextGenerationService.php @@ -0,0 +1,86 @@ +apiKey = config('services.gemini.api_key') ?? ''; + } + + /** + * @param array $history + */ + public function generate(string $prompt, array $history = [], ?Workspace $workspace = null, ?string $imageUrl = null): string + { + if (empty($this->apiKey)) { + throw new \RuntimeException('Gemini API key is not configured. Please set GEMINI_API_KEY in your .env file.'); + } + + $systemPrompt = view('prompts.assistant.system', [ + 'brand_name' => $workspace?->name ?? '', + 'brand_description' => $workspace?->brand_description ?? '', + 'brand_website' => $workspace?->brand_website ?? '', + 'tone' => $workspace?->brand_tone ?? 'professional', + 'voice_notes' => $workspace?->brand_voice_notes ?? '', + 'locale' => app()->getLocale(), + ])->render(); + + $contents = []; + + // Add history + foreach ($history as $message) { + $contents[] = [ + 'role' => data_get($message, 'role') === 'assistant' ? 'model' : 'user', + 'parts' => [['text' => data_get($message, 'content')]], + ]; + } + + // Add current prompt + $parts = [['text' => $prompt]]; + + if ($imageUrl) { + $imageResponse = Http::timeout(30)->get($imageUrl); + if ($imageResponse->successful()) { + $parts[] = [ + 'inlineData' => [ + 'mimeType' => $imageResponse->header('Content-Type', 'image/jpeg'), + 'data' => base64_encode($imageResponse->body()), + ], + ]; + } + } + + $contents[] = ['role' => 'user', 'parts' => $parts]; + + $response = Http::timeout(60) + ->post("https://generativelanguage.googleapis.com/v1beta/models/{$this->model}:generateContent?key={$this->apiKey}", [ + 'systemInstruction' => ['parts' => [['text' => $systemPrompt]]], + 'contents' => $contents, + 'generationConfig' => [ + 'maxOutputTokens' => 2048, + 'temperature' => 0.7, + ], + ]); + + if ($response->failed()) { + Log::error('GeminiTextGenerationService failed', ['body' => $response->body()]); + + throw new \RuntimeException('Failed to generate text. Please try again.'); + } + + return data_get($response->json(), 'candidates.0.content.parts.0.text', ''); + } +} diff --git a/app/Services/Ai/ImageGenerationService.php b/app/Services/Ai/ImageGenerationService.php index 83b459dd..3596a5ee 100644 --- a/app/Services/Ai/ImageGenerationService.php +++ b/app/Services/Ai/ImageGenerationService.php @@ -4,8 +4,11 @@ namespace App\Services\Ai; +use App\Enums\Ai\Orientation; +use App\Enums\Ai\UsageType; use App\Models\AiUsageLog; use App\Models\Workspace; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; @@ -13,22 +16,38 @@ class ImageGenerationService { + private string $baseUrl = 'https://generativelanguage.googleapis.com/v1beta'; + + private string $model = 'gemini-2.5-flash-image'; + private string $apiKey; public function __construct() { - $this->apiKey = config('ai.providers.gemini.api_key', ''); + $this->apiKey = config('services.gemini.api_key') ?? ''; } /** * @return array{id: string, path: string, url: string, mime_type: string, type: string} */ - public function generate(string $prompt, Workspace $workspace, ?string $userId = null, ?string $postId = null): array + public function generate(string $prompt, Workspace $workspace, ?string $userId = null, ?string $postId = null, Orientation $orientation = Orientation::Vertical): array { + if (empty($this->apiKey)) { + throw new \RuntimeException('Gemini API key is not configured. Please set GEMINI_API_KEY in your .env file.'); + } + + $aspectRatio = $orientation->aspectRatio(); + + $fullPrompt = view('prompts.assistant.image', [ + 'prompt' => $prompt, + 'brand_name' => $workspace->name ?? '', + 'tone' => $workspace->brand_tone ?? 'professional', + 'aspect_ratio' => $aspectRatio, + ])->render(); + $response = Http::timeout(120) - ->withHeaders(['Content-Type' => 'application/json']) - ->post("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key={$this->apiKey}", [ - 'contents' => [['parts' => [['text' => $prompt]]]], + ->post("{$this->baseUrl}/models/{$this->model}:generateContent?key={$this->apiKey}", [ + 'contents' => [['parts' => [['text' => $fullPrompt]]]], 'generationConfig' => ['responseModalities' => ['TEXT', 'IMAGE']], ]); @@ -38,54 +57,77 @@ public function generate(string $prompt, Workspace $workspace, ?string $userId = throw new \RuntimeException('Failed to generate image. Please try again.'); } - $parts = data_get($response->json(), 'candidates.0.content.parts', []); + $imageData = $this->extractImageFromResponse($response->json()); + + if (! $imageData) { + Log::warning('ImageGenerationService: no image in response', [ + 'response' => $response->json(), + ]); + + throw new \RuntimeException('No image was generated. Try a different prompt.'); + } + + return DB::transaction(function () use ($imageData, $prompt, $workspace, $userId, $postId) { + $extension = $this->getExtension(data_get($imageData, 'mimeType', 'image/png')); + $mimeType = data_get($imageData, 'mimeType', 'image/png'); + $decoded = base64_decode(data_get($imageData, 'data')); + + $filename = Str::uuid().'.'.$extension; + $path = 'medias/'.$filename; + + Storage::put($path, $decoded); + + $media = $workspace->media()->create([ + 'group_id' => Str::uuid()->toString(), + 'collection' => 'assets', + 'type' => 'image', + 'path' => $path, + 'original_filename' => 'ai-generated.'.$extension, + 'mime_type' => $mimeType, + 'size' => strlen($decoded), + 'order' => 0, + 'meta' => ['ai_generated' => true, 'prompt' => Str::limit($prompt, 200)], + ]); + + AiUsageLog::create([ + 'account_id' => $workspace->account_id, + 'workspace_id' => $workspace->id, + 'user_id' => $userId, + 'post_id' => $postId, + 'type' => UsageType::Image, + 'provider' => 'gemini', + ]); + + return [ + 'id' => $media->id, + 'path' => $media->path, + 'url' => $media->url, + 'mime_type' => $mimeType, + 'type' => 'image', + ]; + }); + } + + private function extractImageFromResponse(array $response): ?array + { + $parts = data_get($response, 'candidates.0.content.parts', []); foreach ($parts as $part) { if (data_get($part, 'inlineData')) { - $imageData = base64_decode(data_get($part, 'inlineData.data')); - $mimeType = data_get($part, 'inlineData.mimeType', 'image/png'); - $extension = match ($mimeType) { - 'image/jpeg' => 'jpg', - 'image/webp' => 'webp', - default => 'png', - }; - - $filename = Str::uuid().'.'.$extension; - $path = 'medias/'.$filename; - - Storage::put($path, $imageData); - - $media = $workspace->media()->create([ - 'group_id' => Str::uuid()->toString(), - 'collection' => 'assets', - 'type' => 'image', - 'path' => $path, - 'original_filename' => 'ai-generated.'.$extension, - 'mime_type' => $mimeType, - 'size' => strlen($imageData), - 'order' => 0, - 'meta' => ['ai_generated' => true, 'prompt' => Str::limit($prompt, 200)], - ]); - - AiUsageLog::create([ - 'account_id' => $workspace->account_id, - 'workspace_id' => $workspace->id, - 'user_id' => $userId, - 'post_id' => $postId, - 'type' => 'image', - 'provider' => 'gemini', - ]); - - return [ - 'id' => $media->id, - 'path' => $media->path, - 'url' => $media->url, - 'mime_type' => $mimeType, - 'type' => 'image', - ]; + return data_get($part, 'inlineData'); } } - throw new \RuntimeException('No image was generated. Try a different prompt.'); + return null; + } + + private function getExtension(string $mimeType): string + { + return match ($mimeType) { + 'image/jpeg' => 'jpg', + 'image/webp' => 'webp', + 'image/gif' => 'gif', + default => 'png', + }; } } diff --git a/app/Services/Ai/IntentDetector.php b/app/Services/Ai/IntentDetector.php index ad0c2067..804b28ba 100644 --- a/app/Services/Ai/IntentDetector.php +++ b/app/Services/Ai/IntentDetector.php @@ -4,34 +4,61 @@ namespace App\Services\Ai; +use App\Enums\Ai\Intent; + class IntentDetector { - public function detect(string $prompt): string + public function detect(string $prompt): Intent { $lower = mb_strtolower($prompt); + if ($this->isProhibited($lower)) { + return Intent::Blocked; + } + $videoKeywords = ['video', 'clip', 'reel', 'animation', 'animate', 'footage']; $imageKeywords = ['image', 'photo', 'picture', 'illustration', 'draw', 'design', 'visual', 'graphic']; $audioKeywords = ['audio', 'voice', 'narrate', 'speak', 'tts', 'voiceover', 'text to speech']; foreach ($videoKeywords as $keyword) { if (str_contains($lower, $keyword)) { - return 'video'; + return Intent::Video; } } foreach ($imageKeywords as $keyword) { if (str_contains($lower, $keyword)) { - return 'image'; + return Intent::Image; } } foreach ($audioKeywords as $keyword) { if (str_contains($lower, $keyword)) { - return 'audio'; + return Intent::Audio; } } - return 'text'; + return Intent::Text; + } + + private function isProhibited(string $lower): bool + { + $prohibited = [ + 'porn', 'xxx', 'nude', 'naked', 'hentai', 'nsfw', + 'cocaine', 'heroin', 'meth', + 'murder', 'suicide', 'self-harm', 'self harm', + 'pedophil', 'child porn', 'underage', + 'terrorist', 'terrorism', + 'racist', 'racism', 'nazi', 'white supremac', + 'gore', 'torture', 'dismember', + ]; + + foreach ($prohibited as $word) { + if (preg_match('/\b'.preg_quote($word, '/').'/i', $lower)) { + return true; + } + } + + return false; } } diff --git a/app/Services/Ai/TextGenerationService.php b/app/Services/Ai/TextGenerationService.php index 30d95d0a..97ce86cc 100644 --- a/app/Services/Ai/TextGenerationService.php +++ b/app/Services/Ai/TextGenerationService.php @@ -5,10 +5,11 @@ namespace App\Services\Ai; use App\Models\Workspace; +use App\Services\Ai\Contracts\TextGenerationInterface; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; -class TextGenerationService +class TextGenerationService implements TextGenerationInterface { private string $apiKey; @@ -16,36 +17,47 @@ class TextGenerationService public function __construct() { - $this->apiKey = config('ai.providers.openai.api_key', ''); + $this->apiKey = config('services.openai.api_key') ?? ''; } /** * @param array $history */ - public function generate(string $prompt, array $history = [], ?Workspace $workspace = null): string + public function generate(string $prompt, array $history = [], ?Workspace $workspace = null, ?string $imageUrl = null): string { + if (empty($this->apiKey)) { + throw new \RuntimeException('OpenAI API key is not configured. Please set OPENAI_API_KEY in your .env file.'); + } + $systemPrompt = view('prompts.assistant.system', [ - 'brandName' => $workspace?->name ?? '', - 'brandDescription' => $workspace?->brand_description ?? '', - 'brandWebsite' => $workspace?->brand_website ?? '', + 'brand_name' => $workspace?->name ?? '', + 'brand_description' => $workspace?->brand_description ?? '', + 'brand_website' => $workspace?->brand_website ?? '', 'tone' => $workspace?->brand_tone ?? 'professional', - 'voiceNotes' => $workspace?->brand_voice_notes ?? '', + 'voice_notes' => $workspace?->brand_voice_notes ?? '', + 'locale' => app()->getLocale(), ])->render(); + if ($imageUrl) { + $userContent = [ + ['type' => 'text', 'text' => $prompt], + ['type' => 'image_url', 'image_url' => ['url' => $imageUrl]], + ]; + } else { + $userContent = $prompt; + } + $messages = [ [ 'role' => 'system', 'content' => $systemPrompt, ], ...$history, - ['role' => 'user', 'content' => $prompt], + ['role' => 'user', 'content' => $userContent], ]; $response = Http::timeout(60) - ->withHeaders([ - 'Authorization' => "Bearer {$this->apiKey}", - 'Content-Type' => 'application/json', - ]) + ->withToken($this->apiKey) ->post("{$this->baseUrl}/chat/completions", [ 'model' => 'gpt-4o', 'messages' => $messages, diff --git a/app/Services/Ai/VideoGenerationService.php b/app/Services/Ai/VideoGenerationService.php index 3daa04d3..5f5f4885 100644 --- a/app/Services/Ai/VideoGenerationService.php +++ b/app/Services/Ai/VideoGenerationService.php @@ -4,8 +4,11 @@ namespace App\Services\Ai; +use App\Enums\Ai\Orientation; +use App\Enums\Ai\UsageType; use App\Models\AiUsageLog; use App\Models\Workspace; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; @@ -13,27 +16,91 @@ class VideoGenerationService { + private string $baseUrl = 'https://generativelanguage.googleapis.com/v1beta'; + + private string $model = 'veo-3.1-generate-preview'; + + private int $maxPollAttempts = 30; + + private int $pollIntervalSeconds = 10; + private string $apiKey; public function __construct() { - $this->apiKey = config('ai.providers.gemini.api_key', ''); + $this->apiKey = config('services.gemini.api_key') ?? ''; } /** * @return array{id: string, path: string, url: string, mime_type: string, type: string} */ - public function generate(string $prompt, Workspace $workspace, ?string $userId = null, ?string $postId = null): array + public function generate(string $prompt, Workspace $workspace, ?string $userId = null, ?string $postId = null, Orientation $orientation = Orientation::Vertical): array + { + if (empty($this->apiKey)) { + throw new \RuntimeException('Gemini API key is not configured. Please set GEMINI_API_KEY in your .env file.'); + } + + $aspectRatio = $orientation->aspectRatio(); + + $fullPrompt = view('prompts.assistant.video', [ + 'prompt' => $prompt, + 'brand_name' => $workspace->name ?? '', + 'tone' => $workspace->brand_tone ?? 'professional', + ])->render(); + + $operationName = $this->startGeneration($fullPrompt, $aspectRatio); + $videoData = $this->pollForCompletion($operationName); + + $decoded = base64_decode($videoData); + + return DB::transaction(function () use ($decoded, $prompt, $workspace, $userId, $postId) { + $filename = Str::uuid().'.mp4'; + $path = 'medias/'.$filename; + + Storage::put($path, $decoded); + + $media = $workspace->media()->create([ + 'group_id' => Str::uuid()->toString(), + 'collection' => 'assets', + 'type' => 'video', + 'path' => $path, + 'original_filename' => 'ai-generated.mp4', + 'mime_type' => 'video/mp4', + 'size' => strlen($decoded), + 'order' => 0, + 'meta' => ['ai_generated' => true, 'prompt' => Str::limit($prompt, 200)], + ]); + + AiUsageLog::create([ + 'account_id' => $workspace->account_id, + 'workspace_id' => $workspace->id, + 'user_id' => $userId, + 'post_id' => $postId, + 'type' => UsageType::Video, + 'provider' => 'veo', + ]); + + return [ + 'id' => $media->id, + 'path' => $media->path, + 'url' => $media->url, + 'mime_type' => 'video/mp4', + 'type' => 'video', + ]; + }); + } + + private function startGeneration(string $prompt, string $aspectRatio = '9:16'): string { $response = Http::timeout(30) - ->withHeaders(['Content-Type' => 'application/json']) - ->post("https://generativelanguage.googleapis.com/v1beta/models/veo-3.0-generate-preview:predictLongRunning?key={$this->apiKey}", [ + ->withHeaders(['x-goog-api-key' => $this->apiKey]) + ->post("{$this->baseUrl}/models/{$this->model}:predictLongRunning", [ 'instances' => [['prompt' => $prompt]], 'parameters' => [ - 'aspectRatio' => '9:16', - 'sampleCount' => 1, + 'aspectRatio' => $aspectRatio, 'durationSeconds' => 8, - 'generateAudio' => true, + 'resolution' => '720p', + 'personGeneration' => 'allow_all', ], ]); @@ -49,64 +116,33 @@ public function generate(string $prompt, Workspace $workspace, ?string $userId = throw new \RuntimeException('No operation returned from video generation API.'); } - $maxAttempts = 30; - $videoData = null; + return $operationName; + } - for ($i = 0; $i < $maxAttempts; $i++) { - sleep(10); + private function pollForCompletion(string $operationName): string + { + for ($i = 0; $i < $this->maxPollAttempts; $i++) { + sleep($this->pollIntervalSeconds); - $statusResponse = Http::timeout(30) - ->get("https://generativelanguage.googleapis.com/v1beta/{$operationName}?key={$this->apiKey}"); + $response = Http::timeout(30) + ->withHeaders(['x-goog-api-key' => $this->apiKey]) + ->get("{$this->baseUrl}/{$operationName}"); - if ($statusResponse->failed()) { + if ($response->failed()) { continue; } - $status = $statusResponse->json(); + $status = $response->json(); if (data_get($status, 'done')) { $videoData = data_get($status, 'response.predictions.0.bytesBase64Encoded'); - break; + + if ($videoData) { + return $videoData; + } } } - if (! $videoData) { - throw new \RuntimeException('Video generation timed out. Please try again.'); - } - - $decoded = base64_decode($videoData); - $filename = Str::uuid().'.mp4'; - $path = 'medias/'.$filename; - - Storage::put($path, $decoded); - - $media = $workspace->media()->create([ - 'group_id' => Str::uuid()->toString(), - 'collection' => 'assets', - 'type' => 'video', - 'path' => $path, - 'original_filename' => 'ai-generated.mp4', - 'mime_type' => 'video/mp4', - 'size' => strlen($decoded), - 'order' => 0, - 'meta' => ['ai_generated' => true, 'prompt' => Str::limit($prompt, 200)], - ]); - - AiUsageLog::create([ - 'account_id' => $workspace->account_id, - 'workspace_id' => $workspace->id, - 'user_id' => $userId, - 'post_id' => $postId, - 'type' => 'video', - 'provider' => 'veo', - ]); - - return [ - 'id' => $media->id, - 'path' => $media->path, - 'url' => $media->url, - 'mime_type' => 'video/mp4', - 'type' => 'video', - ]; + throw new \RuntimeException('Video generation timed out. Please try again.'); } } diff --git a/app/Services/GiphyService.php b/app/Services/GiphyService.php index 7c18e76e..e60ff667 100644 --- a/app/Services/GiphyService.php +++ b/app/Services/GiphyService.php @@ -23,6 +23,10 @@ public function __construct() */ public function search(string $query, int $page = 1): array { + if (empty($this->apiKey)) { + return ['results' => [], 'total' => 0, 'total_pages' => 0]; + } + $perPage = (int) config('app.pagination.default'); $offset = ($page - 1) * $perPage; @@ -57,6 +61,10 @@ public function search(string $query, int $page = 1): array */ public function trending(int $page = 1): array { + if (empty($this->apiKey)) { + return []; + } + $perPage = (int) config('app.pagination.default'); $offset = ($page - 1) * $perPage; diff --git a/app/Services/UnsplashService.php b/app/Services/UnsplashService.php index b62d86f5..5ac1ad9a 100644 --- a/app/Services/UnsplashService.php +++ b/app/Services/UnsplashService.php @@ -23,6 +23,10 @@ public function __construct() */ public function search(string $query, int $page = 1): array { + if (empty($this->accessKey)) { + return ['results' => [], 'total' => 0, 'total_pages' => 0]; + } + $response = Http::timeout(10) ->withHeaders(['Authorization' => "Client-ID {$this->accessKey}"]) ->get("{$this->baseUrl}/search/photos", [ @@ -52,6 +56,10 @@ public function search(string $query, int $page = 1): array */ public function trending(int $page = 1): array { + if (empty($this->accessKey)) { + return []; + } + $response = Http::timeout(10) ->withHeaders(['Authorization' => "Client-ID {$this->accessKey}"]) ->get("{$this->baseUrl}/photos", [ diff --git a/composer.json b/composer.json index e07fa6dd..4a3283aa 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,6 @@ "google/apiclient": "^2.19", "inertiajs/inertia-laravel": "^3.0", "intervention/image": "^4.0", - "laravel/ai": "^0.5.1", "laravel/boost": "^2.0", "laravel/cashier": "^16.2", "laravel/framework": "^13.0", diff --git a/composer.lock b/composer.lock index 3a035c84..3fff1c93 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5640db0bf02709462f946712c0b780d0", + "content-hash": "52999022619dad2de45a9e0f4811110c", "packages": [ { "name": "aws/aws-crt-php", @@ -1840,74 +1840,6 @@ ], "time": "2026-04-07T08:43:15+00:00" }, - { - "name": "laravel/ai", - "version": "v0.5.1", - "source": { - "type": "git", - "url": "https://github.com/laravel/ai.git", - "reference": "bf16555eebc2d78efcc4fa2367a476a91b2e508b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/ai/zipball/bf16555eebc2d78efcc4fa2367a476a91b2e508b", - "reference": "bf16555eebc2d78efcc4fa2367a476a91b2e508b", - "shasum": "" - }, - "require": { - "illuminate/console": "^12.0|^13.0", - "illuminate/container": "^12.0|^13.0", - "illuminate/contracts": "^12.0|^13.0", - "illuminate/filesystem": "^12.0|^13.0", - "illuminate/json-schema": "^12.0|^13.0", - "illuminate/support": "^12.0|^13.0", - "laravel/prompts": "^0.3.6", - "laravel/serializable-closure": "^2.0", - "php": "^8.3", - "prism-php/prism": "^0.100.0" - }, - "require-dev": { - "laravel/pint": "^1.26", - "mockery/mockery": "^1.6.12", - "orchestra/testbench": "^10.6|^11.0", - "pestphp/pest": "^3.0|^4.0", - "pestphp/pest-plugin-laravel": "^3.0|^4.0" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "Laravel\\Ai\\AiServiceProvider" - ] - }, - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "files": [ - "functions.php" - ], - "psr-4": { - "Laravel\\Ai\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "The official AI SDK for Laravel.", - "homepage": "https://github.com/laravel/ai", - "keywords": [ - "ai", - "laravel" - ], - "support": { - "issues": "https://github.com/laravel/ai/issues", - "source": "https://github.com/laravel/ai" - }, - "time": "2026-04-10T18:49:05+00:00" - }, { "name": "laravel/boost", "version": "v2.4.3", @@ -5049,85 +4981,6 @@ ], "time": "2026-03-09T20:33:04+00:00" }, - { - "name": "prism-php/prism", - "version": "v0.100.1", - "source": { - "type": "git", - "url": "https://github.com/prism-php/prism.git", - "reference": "5d6cc65b80b19cf3f22744703ac0c727b68cdca8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/prism-php/prism/zipball/5d6cc65b80b19cf3f22744703ac0c727b68cdca8", - "reference": "5d6cc65b80b19cf3f22744703ac0c727b68cdca8", - "shasum": "" - }, - "require": { - "ext-fileinfo": "*", - "laravel/framework": "^11.0|^12.0|^13.0", - "php": "^8.2" - }, - "require-dev": { - "brianium/paratest": "^7.8.4", - "laravel/mcp": "^0.6.0", - "laravel/pint": "^1.14", - "mockery/mockery": "^1.6", - "orchestra/testbench": "^9|^10|^11", - "pestphp/pest": "^3.0|^4.0", - "pestphp/pest-plugin-arch": "^3.0|^4.0", - "pestphp/pest-plugin-laravel": "^3.0|^4.0", - "phpstan/extension-installer": "^1.3", - "phpstan/phpdoc-parser": "^2.0", - "phpstan/phpstan": "2.1.34", - "phpstan/phpstan-deprecation-rules": "^2.0", - "projektgopher/whisky": "^0.7.0", - "rector/rector": "2.3.3", - "spatie/laravel-ray": "^1.39", - "symplify/rule-doc-generator-contracts": "^11.2" - }, - "type": "library", - "extra": { - "laravel": { - "aliases": { - "PrismServer": "Prism\\Prism\\Facades\\PrismServer" - }, - "providers": [ - "Prism\\Prism\\PrismServiceProvider" - ] - } - }, - "autoload": { - "files": [ - "src/helpers.php" - ], - "psr-4": { - "Prism\\Prism\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "TJ Miller", - "email": "hello@echolabs.dev" - } - ], - "description": "A powerful Laravel package for integrating Large Language Models (LLMs) into your applications.", - "support": { - "issues": "https://github.com/prism-php/prism/issues", - "source": "https://github.com/prism-php/prism/tree/v0.100.1" - }, - "funding": [ - { - "url": "https://github.com/sixlive", - "type": "github" - } - ], - "time": "2026-03-20T20:37:17+00:00" - }, { "name": "psr/cache", "version": "3.0.0", diff --git a/config/ai.php b/config/ai.php deleted file mode 100644 index fd9c922f..00000000 --- a/config/ai.php +++ /dev/null @@ -1,132 +0,0 @@ - 'openai', - 'default_for_images' => 'gemini', - 'default_for_audio' => 'openai', - 'default_for_transcription' => 'openai', - 'default_for_embeddings' => 'openai', - 'default_for_reranking' => 'cohere', - - /* - |-------------------------------------------------------------------------- - | Caching - |-------------------------------------------------------------------------- - | - | Below you may configure caching strategies for AI related operations - | such as embedding generation. You are free to adjust these values - | based on your application's available caching stores and needs. - | - */ - - 'caching' => [ - 'embeddings' => [ - 'cache' => false, - 'store' => env('CACHE_STORE', 'database'), - ], - ], - - /* - |-------------------------------------------------------------------------- - | AI Providers - |-------------------------------------------------------------------------- - | - | Below are each of your AI providers defined for this application. Each - | represents an AI provider and API key combination which can be used - | to perform tasks like text, image, and audio creation via agents. - | - */ - - 'providers' => [ - 'anthropic' => [ - 'driver' => 'anthropic', - 'key' => env('ANTHROPIC_API_KEY'), - ], - - 'azure' => [ - 'driver' => 'azure', - 'key' => env('AZURE_OPENAI_API_KEY'), - 'url' => env('AZURE_OPENAI_URL'), - 'api_version' => env('AZURE_OPENAI_API_VERSION', '2024-10-21'), - 'deployment' => env('AZURE_OPENAI_DEPLOYMENT', 'gpt-4o'), - 'embedding_deployment' => env('AZURE_OPENAI_EMBEDDING_DEPLOYMENT', 'text-embedding-3-small'), - ], - - 'cohere' => [ - 'driver' => 'cohere', - 'key' => env('COHERE_API_KEY'), - ], - - 'deepseek' => [ - 'driver' => 'deepseek', - 'key' => env('DEEPSEEK_API_KEY'), - ], - - 'eleven' => [ - 'driver' => 'eleven', - 'key' => env('ELEVENLABS_API_KEY'), - ], - - 'gemini' => [ - 'driver' => 'gemini', - 'key' => env('GEMINI_API_KEY'), - ], - - 'groq' => [ - 'driver' => 'groq', - 'key' => env('GROQ_API_KEY'), - ], - - 'jina' => [ - 'driver' => 'jina', - 'key' => env('JINA_API_KEY'), - ], - - 'mistral' => [ - 'driver' => 'mistral', - 'key' => env('MISTRAL_API_KEY'), - ], - - 'ollama' => [ - 'driver' => 'ollama', - 'key' => env('OLLAMA_API_KEY', ''), - 'url' => env('OLLAMA_BASE_URL', 'http://localhost:11434'), - ], - - 'openai' => [ - 'driver' => 'openai', - 'key' => env('OPENAI_API_KEY'), - 'url' => env('OPENAI_URL', 'https://api.openai.com/v1'), - ], - - 'openrouter' => [ - 'driver' => 'openrouter', - 'key' => env('OPENROUTER_API_KEY'), - ], - - 'voyageai' => [ - 'driver' => 'voyageai', - 'key' => env('VOYAGEAI_API_KEY'), - ], - - 'xai' => [ - 'driver' => 'xai', - 'key' => env('XAI_API_KEY'), - ], - ], - -]; diff --git a/config/services.php b/config/services.php index 242136e5..e84f6b4c 100644 --- a/config/services.php +++ b/config/services.php @@ -122,6 +122,14 @@ 'api_key' => env('GIPHY_API_KEY'), ], + 'openai' => [ + 'api_key' => env('OPENAI_API_KEY'), + ], + + 'gemini' => [ + 'api_key' => env('GEMINI_API_KEY'), + ], + 'elevenlabs' => [ 'api_key' => env('ELEVENLABS_API_KEY'), 'default_voice' => env('ELEVENLABS_DEFAULT_VOICE', 'EXAVITQu4vr4xnSDxMaL'), diff --git a/config/trypost.php b/config/trypost.php index 32ce9b60..1db35fc5 100644 --- a/config/trypost.php +++ b/config/trypost.php @@ -78,4 +78,21 @@ ], ], + /* + |-------------------------------------------------------------------------- + | AI Configuration + |-------------------------------------------------------------------------- + | + | Configure which AI providers to use for content generation. + | Supported text providers: "gemini", "openai" + | + */ + + 'ai' => [ + 'text_provider' => env('AI_TEXT_PROVIDER', 'gemini'), + 'image_provider' => env('AI_IMAGE_PROVIDER', 'gemini'), + 'audio_provider' => env('AI_AUDIO_PROVIDER', 'elevenlabs'), + 'video_provider' => env('AI_VIDEO_PROVIDER', 'gemini'), + ], + ]; diff --git a/database/factories/AiUsageLogFactory.php b/database/factories/AiUsageLogFactory.php index fead0e65..d947f4f3 100644 --- a/database/factories/AiUsageLogFactory.php +++ b/database/factories/AiUsageLogFactory.php @@ -4,6 +4,7 @@ namespace Database\Factories; +use App\Enums\Ai\UsageType; use App\Models\Account; use App\Models\AiUsageLog; use App\Models\Workspace; @@ -17,7 +18,7 @@ public function definition(): array return [ 'account_id' => Account::factory(), 'workspace_id' => Workspace::factory(), - 'type' => fake()->randomElement(['image', 'video', 'audio']), + 'type' => fake()->randomElement(UsageType::cases()), 'provider' => fake()->randomElement(['gemini', 'veo', 'elevenlabs']), ]; } @@ -25,7 +26,7 @@ public function definition(): array public function image(): static { return $this->state(fn () => [ - 'type' => 'image', + 'type' => UsageType::Image, 'provider' => 'gemini', ]); } @@ -33,7 +34,7 @@ public function image(): static public function video(): static { return $this->state(fn () => [ - 'type' => 'video', + 'type' => UsageType::Video, 'provider' => 'veo', ]); } @@ -41,7 +42,7 @@ public function video(): static public function audio(): static { return $this->state(fn () => [ - 'type' => 'audio', + 'type' => UsageType::Audio, 'provider' => 'elevenlabs', ]); } diff --git a/database/migrations/2026_03_29_232520_create_agent_conversations_table.php b/database/migrations/2026_03_29_232520_create_agent_conversations_table.php deleted file mode 100644 index 4fe11658..00000000 --- a/database/migrations/2026_03_29_232520_create_agent_conversations_table.php +++ /dev/null @@ -1,52 +0,0 @@ -uuid('id')->primary(); - $table->foreignId('user_id')->nullable(); - $table->string('title'); - $table->timestamps(); - - $table->index(['user_id', 'updated_at']); - }); - - Schema::create('agent_conversation_messages', function (Blueprint $table) { - $table->uuid('id')->primary(); - $table->string('conversation_id', 36)->index(); - $table->foreignId('user_id')->nullable(); - $table->string('agent'); - $table->string('role', 25); - $table->text('content'); - $table->text('attachments'); - $table->text('tool_calls'); - $table->text('tool_results'); - $table->text('usage'); - $table->text('meta'); - $table->timestamps(); - - $table->index(['conversation_id', 'user_id', 'updated_at'], 'conversation_index'); - $table->index(['user_id']); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('agent_conversations'); - Schema::dropIfExists('agent_conversation_messages'); - } -}; diff --git a/lang/en/assistant.php b/lang/en/assistant.php index ab4554b0..6415d5e0 100644 --- a/lang/en/assistant.php +++ b/lang/en/assistant.php @@ -12,4 +12,5 @@ 'empty' => 'Ask me anything. I can write captions, generate images, create audio, and produce videos.', 'limit_reached_images' => 'You have reached your monthly image generation limit.', 'limit_reached_videos' => 'You have reached your monthly video generation limit.', + 'content_blocked' => "I can't help with that type of content. I'm here to help you create safe, engaging social media content.", ]; diff --git a/lang/php_en.json b/lang/php_en.json deleted file mode 100644 index df500a11..00000000 --- a/lang/php_en.json +++ /dev/null @@ -1 +0,0 @@ -{"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":"Social Accounts","accounts.description":"Overview of all your connected social accounts","accounts.add_social":"Add Social","accounts.add_social_title":"Connect a Social Account","accounts.add_social_description":"Connect a social account to TryPost to start posting","accounts.no_accounts":"No accounts connected yet","accounts.no_accounts_description":"Connect your social networks to start scheduling and publishing posts","accounts.added":"Added :date","accounts.limit_reached":"You have reached your plan limit for social accounts.","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.descriptions.linkedin":"Connect your LinkedIn personal profile","accounts.descriptions.linkedin-page":"Connect a LinkedIn company page","accounts.descriptions.x":"Connect your X (Twitter) account","accounts.descriptions.tiktok":"Connect your TikTok account","accounts.descriptions.youtube":"Connect a YouTube channel","accounts.descriptions.facebook":"Connect a Facebook page","accounts.descriptions.instagram":"Connect an Instagram professional account","accounts.descriptions.instagram-facebook":"Connect Instagram via Facebook page","accounts.descriptions.threads":"Connect your Threads account","accounts.descriptions.pinterest":"Connect your Pinterest account","accounts.descriptions.bluesky":"Connect your Bluesky account","accounts.descriptions.mastodon":"Connect your Mastodon account","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.instagram_facebook.title":"Select Instagram Account","accounts.instagram_facebook.description":"Choose which Instagram account you want to connect","accounts.instagram_facebook.no_pages":"No Instagram accounts found","accounts.instagram_facebook.no_pages_description":"No Facebook Pages with linked Instagram Business accounts were found.","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.activated":"Account activated!","accounts.flash.deactivated":"Account deactivated!","accounts.flash.already_connected":"This platform is already connected.","accounts.flash.no_youtube_channels":"No YouTube channels found. Please create a channel first.","analytics.channels":"Channels","analytics.no_accounts":"No connected accounts with analytics.","analytics.select_account":"Select an account to view analytics.","analytics.no_data":"No analytics data available.","assets.title":"Assets","assets.tabs.my_uploads":"My Uploads","assets.tabs.stock_photos":"Stock Photos","assets.tabs.gifs":"GIFs","assets.upload.drag_drop":"Drag & drop your files here, or click to select","assets.upload.formats":"JPEG, PNG, GIF, WebP, MP4","assets.upload.uploading":"Uploading...","assets.empty.title":"No assets yet","assets.empty.description":"Upload images and videos to build your media library.","assets.save_to_assets":"Save to Assets","assets.saved":"Saved to your assets!","assets.create_post":"Create post","assets.delete.title":"Delete asset","assets.delete.description":"Are you sure you want to delete this asset? This action cannot be undone.","assets.delete.confirm":"Delete","assets.delete.cancel":"Cancel","assets.unsplash.search_placeholder":"Search free photos...","assets.unsplash.no_results":"No photos found","assets.unsplash.no_results_description":"Try a different search term.","assets.unsplash.trending":"Trending on Unsplash","assets.unsplash.start_searching":"Search for free stock photos from Unsplash","assets.giphy.trending":"Trending on Giphy","assets.giphy.search_placeholder":"Search GIFs...","assets.giphy.no_results":"No GIFs found","assets.giphy.no_results_description":"Try a different search term.","assets.giphy.powered_by":"Powered by GIPHY","assistant.placeholder":"Ask me to write a caption, generate an image, create audio...","assistant.thinking":"Thinking...","assistant.add_to_post":"Add to post","assistant.added":"Added","assistant.error":"Something went wrong. Please try again.","assistant.image_generated":"Here is the generated image:","assistant.video_generated":"Here is the generated video:","assistant.audio_generated":"Here is the generated audio:","assistant.empty":"Ask me anything. I can write captions, generate images, create audio, and produce videos.","assistant.limit_reached_images":"You have reached your monthly image generation limit.","assistant.limit_reached_videos":"You have reached your monthly video generation limit.","auth.flash.welcome":"Welcome to TryPost!","auth.flash.welcome_trial":"Welcome to TryPost! Your trial has started.","auth.legal":"By continuing, you agree to our Terms of Service and Privacy Policy.","auth.slides.calendar.title":"Visual Calendar","auth.slides.calendar.description":"Plan and schedule your content with an intuitive drag-and-drop calendar across all your social accounts.","auth.slides.scheduling.title":"Smart Scheduling","auth.slides.scheduling.description":"Schedule posts across LinkedIn, X, Instagram, TikTok, YouTube, and more — all from one place.","auth.slides.media.title":"Rich Media","auth.slides.media.description":"Publish images, carousels, stories, and reels. Each platform gets the right format automatically.","auth.slides.video.title":"Video Publishing","auth.slides.video.description":"Upload videos once and publish to TikTok, YouTube Shorts, Instagram Reels, and Facebook Reels.","auth.slides.team.title":"Team Workspaces","auth.slides.team.description":"Invite your team, assign roles, and manage multiple brands from separate workspaces.","auth.slides.hashtags.title":"Hashtag Groups","auth.slides.hashtags.description":"Save hashtag collections and add them to posts with one click. Organize with labels and filters.","auth.or_continue_with":"Or continue with","auth.google_login":"Log in with Google","auth.google_signup":"Sign up with Google","auth.signup_success.page_title":"Welcome","auth.signup_success.title":"Setting up your account","auth.signup_success.description":"This usually takes just a few seconds...","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.show_password":"Show password","auth.register.hide_password":"Hide 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":"Billing","billing.subscribe.page_title":"Choose your plan","billing.subscribe.title":"Choose the right plan for you","billing.subscribe.description":"Start with a :days-day free trial. No charge until your trial ends.","billing.subscribe.trial_info":":days-day free trial, then billed automatically","billing.subscribe.monthly":"Monthly","billing.subscribe.yearly":"Yearly","billing.subscribe.per_month":"mo","billing.subscribe.billed_monthly":"billed monthly","billing.subscribe.billed_yearly":"billed yearly","billing.subscribe.save_months":"2 months free","billing.subscribe.popular":"Most popular","billing.subscribe.start_trial":"Start :days-day free trial","billing.subscribe.card_required":"Credit card required to start your trial.","billing.subscribe.cancel_anytime":"Cancel anytime before your trial ends — no charge.","billing.subscribe.features.social_accounts":":count social accounts","billing.subscribe.features.workspaces":":count workspaces","billing.subscribe.features.members":":count team members","billing.subscribe.features.ai_images":":count AI images/mo","billing.subscribe.features.ai_videos":":count AI videos/mo","billing.subscribe.features.data_retention":":days data retention","billing.plan.title":"Plan","billing.plan.description":"Manage your subscription plan.","billing.plan.change":"Change plan","billing.plan.label":"Plan","billing.plan.price":"Price","billing.plan.month":"month","billing.plan.trial":"Trial","billing.plan.active":"Active","billing.plan.past_due":"Past due","billing.plan.cancelling":"Cancelling","billing.plan.trial_ends":"Trial ends","billing.subscription.title":"Subscription","billing.subscription.description":"Manage your payment method, billing details, and subscription.","billing.subscription.payment_method":"Payment method","billing.subscription.manage_label":"Subscription","billing.subscription.manage_stripe":"Manage on Stripe","billing.invoices.title":"Invoices","billing.invoices.description":"Download your past invoices.","billing.invoices.empty":"No invoices found","billing.invoices.paid":"Paid","billing.processing.page_title":"Processing...","billing.processing.title":"Processing your subscription","billing.processing.description":"Please wait while we set up your account. This will only take a moment.","billing.processing.success_title":"You're all set!","billing.processing.success_description":"Your subscription is active. Redirecting you to your workspaces...","billing.processing.cancelled_title":"Checkout cancelled","billing.processing.cancelled_description":"Your checkout was cancelled. No charges were made.","billing.processing.retry":"Try again","brands.new_brand":"New Brand","brands.no_brands_yet":"No brands yet","brands.no_brands_description":"Create brands to organize your social accounts by client or project","brands.accounts_count":":count accounts","brands.create.title":"Create Brand","brands.create.description":"Give your brand a name to group social accounts","brands.create.name":"Brand Name","brands.create.name_placeholder":"e.g. Acme Corp, Personal","brands.create.submit":"Create Brand","brands.create.submitting":"Creating...","brands.edit.title":"Edit Brand","brands.edit.description":"Update the name of this brand","brands.edit.name":"Brand Name","brands.edit.name_placeholder":"e.g. Acme Corp, Personal","brands.edit.submit":"Save Changes","brands.edit.submitting":"Saving...","brands.delete.title":"Delete Brand","brands.delete.description":"Are you sure you want to delete this brand? Social accounts will be unassigned but not deleted.","brands.delete.confirm":"Delete","brands.delete.cancel":"Cancel","brands.flash.created":"Brand created successfully!","brands.flash.updated":"Brand updated successfully!","brands.flash.deleted":"Brand deleted successfully!","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","comments.placeholder":"Write a comment...","comments.reply_placeholder":"Write a reply...","comments.reply":"Reply","comments.edit":"Edit","comments.delete":"Delete","comments.edited":"edited","comments.save":"Save","comments.cancel":"Cancel","comments.send":"Send","comments.replying_to":"Replying to :name","comments.empty":"No comments yet. Start the conversation.","comments.load_more":"Load older comments","common.confirm_modal.cannot_be_undone":"This cannot be undone.","common.confirm_modal.type":"Type","common.confirm_modal.to_confirm":"to confirm.","common.confirm_modal.copy_to_clipboard":"Copy to clipboard","common.photo_upload.upload":"Upload","common.photo_upload.uploading":"Uploading...","common.photo_upload.remove":"Remove photo","common.photo_upload.hint":"Recommended: square image, max 2 MB.","common.timezone.select":"Select timezone","common.timezone.search":"Search timezone...","common.timezone.empty":"No timezone found","common.date_picker.select":"Select date","common.cancel":"Cancel","hashtags.title":"Hashtags","hashtags.description":"Create hashtag groups to quickly add to your posts","hashtags.search":"Search hashtags...","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.search":"Search labels...","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","onboarding.role.page_title":"Welcome - Tell us about yourself","onboarding.role.title":"Tell us about yourself","onboarding.role.description":"Help us personalize your experience","onboarding.role.submit":"Continue","onboarding.personas.founder.label":"Founder","onboarding.personas.founder.description":"Building a startup or new venture","onboarding.personas.creator.label":"Creator","onboarding.personas.creator.description":"Content creator or influencer","onboarding.personas.agency.label":"Agency","onboarding.personas.agency.description":"Marketing or social media agency","onboarding.personas.enterprise.label":"Enterprise","onboarding.personas.enterprise.description":"Large company or corporation","onboarding.personas.small_business.label":"Small Business","onboarding.personas.small_business.description":"Small to medium business","onboarding.personas.personal.label":"Personal","onboarding.personas.personal.description":"Personal brand or hobby","onboarding.connect.page_title":"Connect your accounts","onboarding.connect.title":"Connect your accounts","onboarding.connect.description":"Connect at least one social network to get started","onboarding.connect.submit":"Continue","onboarding.connect.error":"Something went wrong. Please try again.","onboarding.connect.go_back":"Go Back","posts.title":"Posts","posts.search":"Search 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.form.tiktok.settings":"TikTok Settings","posts.form.tiktok.privacy_level":"Who can see this video?","posts.form.tiktok.privacy.public":"Public to everyone","posts.form.tiktok.privacy.friends":"Mutual follow friends","posts.form.tiktok.privacy.followers":"Followers","posts.form.tiktok.privacy.private":"Only me","posts.form.tiktok.privacy_hint":"The available options depend on your TikTok account settings.","posts.form.tiktok.auto_add_music":"Auto add music","posts.form.tiktok.auto_add_music_hint":"This feature is available only for photos. It will add a default music that you can change later.","posts.form.tiktok.yes":"Yes","posts.form.tiktok.no":"No","posts.form.tiktok.allow_users":"Allow users to:","posts.form.tiktok.comments":"Comment","posts.form.tiktok.duet":"Duet","posts.form.tiktok.stitch":"Stitch","posts.form.tiktok.is_aigc":"Video made with AI","posts.form.tiktok.brand_content":"Disclose paid partnership","posts.form.tiktok.brand_content_hint":"This video promotes a third-party business, brand, or product.","posts.form.tiktok.brand_organic":"Disclose your own brand","posts.form.tiktok.brand_organic_hint":"This video promotes your own business, brand, or product.","posts.status.pending":"Pending","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.labels":"Labels","posts.edit.no_labels":"No labels created yet","posts.edit.schedule":"Schedule","posts.edit.pick_time":"Pick time","posts.edit.post_now":"Post now","posts.edit.delete":"Delete","posts.edit.schedule_for":"Schedule for","posts.edit.schedule_date":"Schedule date","posts.edit.saving":"Saving...","posts.edit.saved":"Saved","posts.edit.media":"Media","posts.edit.caption":"Caption","posts.edit.caption_placeholder":"Write your caption...","posts.edit.drag_drop":"Drag & drop or click to upload","posts.edit.publish_to":"Publish to","posts.edit.organize":"Organize","posts.edit.hashtags":"Hashtags","posts.edit.view_on_platform":"View on platform","posts.edit.platform_status":"Platform status","posts.edit.tabs.preview":"Preview","posts.edit.tabs.schedule":"Schedule","posts.edit.tabs.comments":"Comments","posts.edit.tabs.comments_empty":"No comments yet.","posts.edit.tabs.writing_assistant":"AI Assistant","posts.edit.tabs.writing_assistant_empty":"AI writing assistant coming soon.","posts.edit.status.published":"Published","posts.edit.status.publishing":"Publishing...","posts.edit.status.failed":"Failed","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.requires_content":"Text content is required","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","posts.errors.account_inactive":"Social account is deactivated","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.notifications":"Notifications","settings.nav.billing":"Billing","settings.notifications.title":"Notification preferences","settings.notifications.heading":"Email notifications","settings.notifications.description":"Choose which email notifications you want to receive","settings.notifications.post_published":"Post published","settings.notifications.post_published_description":"Receive an email when your post is published successfully","settings.notifications.post_failed":"Post failed","settings.notifications.post_failed_description":"Receive an email when your post fails to publish","settings.notifications.account_disconnected":"Account disconnected","settings.notifications.account_disconnected_description":"Receive an email when a social account is disconnected","settings.notifications.save":"Save preferences","settings.profile.title":"Profile settings","settings.profile.photo_heading":"Profile photo","settings.profile.photo_description":"Upload a profile photo","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.logo_heading":"Workspace logo","settings.workspace.logo_description":"Upload a logo for your workspace","settings.workspace.heading":"Workspace name","settings.workspace.description":"Update your workspace name and timezone","settings.workspace.members_heading":"Members","settings.workspace.members_description":"Manage workspace members and invitations","settings.workspace.name":"Name","settings.workspace.name_placeholder":"My Workspace","settings.workspace.timezone":"Timezone","settings.workspace.save":"Save","settings.brand.title":"Brand","settings.brand.description":"Configure your brand identity for AI-generated content.","settings.brand.website":"Website","settings.brand.website_placeholder":"https://yourbrand.com","settings.brand.brand_description":"Description","settings.brand.brand_description_placeholder":"Tell us about your brand, what you do, and who your audience is...","settings.brand.tone":"Tone of voice","settings.brand.tone_professional":"Professional","settings.brand.tone_casual":"Casual","settings.brand.tone_friendly":"Friendly","settings.brand.tone_bold":"Bold","settings.brand.tone_inspirational":"Inspirational","settings.brand.tone_humorous":"Humorous","settings.brand.tone_educational":"Educational","settings.brand.voice_notes":"Voice notes","settings.brand.voice_notes_placeholder":"Additional writing guidelines, words to avoid, style preferences...","settings.members.title":"Members","settings.members.heading":"Team members","settings.members.description":"Manage members and invites for this workspace","settings.members.cancel":"Cancel","settings.members.remove":"Remove","settings.members.make_admin":"Make admin","settings.members.make_member":"Make member","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.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.remove_modal.title":"Remove member","settings.members.remove_modal.description":"Are you sure you want to remove this member from the workspace? They will lose access to all workspace resources.","settings.members.remove_modal.action":"Remove member","settings.members.cancel_invite_modal.title":"Cancel invitation","settings.members.cancel_invite_modal.description":"Are you sure you want to cancel this invitation?","settings.members.cancel_invite_modal.action":"Cancel invitation","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.role_updated":"Member role updated.","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.account.title":"Account Settings","settings.account.description":"Manage your account name and billing email","settings.account.name":"Account Name","settings.account.name_placeholder":"My Company","settings.account.billing_email":"Billing Email","settings.account.billing_email_placeholder":"billing@company.com","settings.account.billing_email_hint":"This email will be used for invoices and billing communications from Stripe.","settings.account.submit":"Save","settings.flash.account_updated":"Account updated successfully!","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!","settings.flash.photo_updated":"Photo updated successfully!","settings.flash.photo_deleted":"Photo removed successfully!","settings.flash.logo_updated":"Logo uploaded successfully!","settings.flash.logo_deleted":"Logo removed successfully!","settings.flash.notifications_updated":"Notification preferences updated!","settings.api_keys.title":"API Keys","settings.api_keys.page_title":"API Keys","settings.api_keys.heading":"API Keys","settings.api_keys.description":"Manage API keys for programmatic access to your workspace.","settings.api_keys.create":"Create API Key","settings.api_keys.copy":"Copy","settings.api_keys.new_token_message":"Your new API key has been created. Copy it now — you won't be able to see it again.","settings.api_keys.table.name":"Name","settings.api_keys.table.key":"Key","settings.api_keys.table.status":"Status","settings.api_keys.table.expires":"Expires","settings.api_keys.table.last_used":"Last Used","settings.api_keys.table.never":"Never","settings.api_keys.actions.copy_id":"Copy API Key ID","settings.api_keys.actions.copy_id_success":"API Key ID copied to clipboard","settings.api_keys.actions.delete":"Delete","settings.api_keys.empty.title":"No API keys yet","settings.api_keys.empty.description":"Create an API key to access your workspace programmatically.","settings.api_keys.delete_modal.title":"Delete API key","settings.api_keys.delete_modal.description":"Are you sure you want to delete this API key? Any applications using this key will lose access immediately.","settings.api_keys.delete_modal.action":"Delete API key","settings.api_keys.create_dialog.title":"Create API Key","settings.api_keys.create_dialog.description":"Create a new API key for programmatic access to your workspace.","settings.api_keys.create_dialog.name":"Name","settings.api_keys.create_dialog.name_placeholder":"e.g. Production API Key","settings.api_keys.create_dialog.expires":"Expiration date (optional)","settings.api_keys.create_dialog.expires_placeholder":"No expiration","settings.api_keys.create_dialog.submit":"Create","settings.api_keys.create_dialog.cancel":"Cancel","settings.api_keys.flash.created":"API key created successfully!","settings.api_keys.flash.deleted":"API key deleted successfully!","sidebar.workspaces":"Workspaces","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.connections":"Connections","sidebar.workspace.hashtags":"Hashtags","sidebar.workspace.labels":"Labels","sidebar.workspace.assets":"Assets","sidebar.workspace.api_keys":"API Keys","sidebar.workspace.settings":"Settings","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.workspace":"Workspace","sidebar.groups.account":"Account","sidebar.groups.support":"Support","sidebar.analytics":"Analytics","sidebar.posts.calendar":"Calendar","sidebar.posts.all":"All","sidebar.posts.scheduled":"Scheduled","sidebar.posts.posted":"Posted","sidebar.posts.drafts":"Drafts","sidebar.account.settings":"Settings","sidebar.account.usage":"Usage","sidebar.account.billing":"Billing","sidebar.notifications":"Notifications","sidebar.mark_all_read":"Mark all as read","sidebar.mark_as_read":"Mark as read","sidebar.archive_all":"Archive all","sidebar.no_notifications":"No notifications","sidebar.support.discord":"Discord","sidebar.support.share_feedback":"Share feedback","sidebar.support.last_updates":"Last Updates","sidebar.support.docs":"Documentation","usage.title":"Usage","usage.section_account":"Account","usage.section_account_description":"Quotas and limits for your :plan plan.","usage.section_ai":"AI Generation","usage.section_ai_description":"AI image and video generation usage for the current month.","usage.section_data":"Data","usage.section_data_description":"Data retention and storage for your plan.","usage.workspaces":"Workspaces","usage.social_accounts":"Social Accounts","usage.members":"Members","usage.ai_images":"Images","usage.ai_videos":"Videos","usage.data_retention":"Data Retention","usage.unlimited":"Unlimited","usage.days":"days","usage.year":"year","usage.years":"years","workspaces.title":"Workspaces","workspaces.select_title":"Your workspaces","workspaces.select_description":"Select a workspace to continue","workspaces.current":"Current","workspaces.connections":":count connections","workspaces.posts":":count posts","workspaces.create.title":"Create a workspace","workspaces.create.description":"Give your workspace a name to get started","workspaces.create.name":"Workspace name","workspaces.create.name_placeholder":"My Workspace","workspaces.create.submit":"Create workspace"} \ No newline at end of file diff --git a/lang/php_es.json b/lang/php_es.json deleted file mode 100644 index f53948c2..00000000 --- a/lang/php_es.json +++ /dev/null @@ -1 +0,0 @@ -{"accounts.title":"Conexiones","accounts.page_title":"Cuentas Sociales","accounts.description":"Resumen de todas tus cuentas sociales conectadas","accounts.add_social":"Agregar Red Social","accounts.add_social_title":"Conectar una Cuenta Social","accounts.add_social_description":"Conecta una cuenta social a TryPost para empezar a publicar","accounts.no_accounts":"No hay cuentas conectadas todavía","accounts.no_accounts_description":"Conecta tus redes sociales para empezar a programar y publicar posts","accounts.added":"Agregada :date","accounts.limit_reached":"Has alcanzado el límite de cuentas sociales de tu plan.","accounts.not_connected":"No conectado","accounts.connect":"Conectar","accounts.connection_lost":"Conexión perdida","accounts.reconnect_account":"Reconectar cuenta","accounts.view_profile":"Ver perfil","accounts.disconnect":"Desconectar","accounts.descriptions.linkedin":"Conecta tu perfil personal de LinkedIn","accounts.descriptions.linkedin-page":"Conecta una página de empresa de LinkedIn","accounts.descriptions.x":"Conecta tu cuenta de X (Twitter)","accounts.descriptions.tiktok":"Conecta tu cuenta de TikTok","accounts.descriptions.youtube":"Conecta un canal de YouTube","accounts.descriptions.facebook":"Conecta una página de Facebook","accounts.descriptions.instagram":"Conecta una cuenta profesional de Instagram","accounts.descriptions.instagram-facebook":"Conecta Instagram vía página de Facebook","accounts.descriptions.threads":"Conecta tu cuenta de Threads","accounts.descriptions.pinterest":"Conecta tu cuenta de Pinterest","accounts.descriptions.bluesky":"Conecta tu cuenta de Bluesky","accounts.descriptions.mastodon":"Conecta tu cuenta de Mastodon","accounts.disconnect_modal.title":"Desconectar cuenta","accounts.disconnect_modal.description":"¿Estás seguro de que deseas desconectar esta cuenta? Puedes volver a conectarla en cualquier momento.","accounts.disconnect_modal.confirm":"Desconectar","accounts.disconnect_modal.cancel":"Cancelar","accounts.bluesky.title":"Conectar Bluesky","accounts.bluesky.description":"Introduce tus credenciales para conectar","accounts.bluesky.email":"Correo electrónico","accounts.bluesky.email_placeholder":"tuusuario.bsky.social","accounts.bluesky.app_password":"Contraseña de app","accounts.bluesky.app_password_placeholder":"xxxx-xxxx-xxxx-xxxx","accounts.bluesky.app_password_hint":"Usa una Contraseña de App por seguridad. Crea una en bsky.app/settings.","accounts.bluesky.submit":"Conectar Bluesky","accounts.bluesky.submitting":"Conectando...","accounts.mastodon.title":"Conectar Mastodon","accounts.mastodon.description":"Introduce tu instancia de Mastodon","accounts.mastodon.instance_url":"URL de la instancia","accounts.mastodon.instance_placeholder":"https://mastodon.social","accounts.mastodon.instance_hint":"Introduce la URL de tu instancia de Mastodon (ej: mastodon.social, techhub.social)","accounts.mastodon.submit":"Continuar con Mastodon","accounts.mastodon.submitting":"Conectando...","accounts.facebook.title":"Seleccionar página de Facebook","accounts.facebook.description":"Elige qué página deseas conectar","accounts.facebook.no_pages":"No se encontraron páginas","accounts.facebook.no_pages_description":"No eres administrador de ninguna página de Facebook.","accounts.facebook.page_label":"Página de Facebook","accounts.instagram_facebook.title":"Seleccionar cuenta de Instagram","accounts.instagram_facebook.description":"Elige qué cuenta de Instagram deseas conectar","accounts.instagram_facebook.no_pages":"No se encontraron cuentas de Instagram","accounts.instagram_facebook.no_pages_description":"No se encontraron páginas de Facebook con cuentas Instagram Business vinculadas.","accounts.linkedin.title":"Seleccionar página de LinkedIn","accounts.linkedin.description":"Elige qué página deseas conectar","accounts.linkedin.no_pages":"No se encontraron páginas","accounts.linkedin.no_pages_description":"No eres administrador de ninguna página de LinkedIn.","accounts.linkedin.page_label":"Página de LinkedIn","accounts.flash.disconnected":"¡Cuenta desconectada correctamente!","accounts.flash.connected":"¡Cuenta conectada correctamente!","accounts.flash.session_expired":"Sesión expirada. Inténtalo de nuevo.","accounts.flash.workspace_not_found":"Workspace no encontrado.","accounts.flash.activated":"¡Cuenta activada!","accounts.flash.deactivated":"¡Cuenta desactivada!","accounts.flash.already_connected":"Esta plataforma ya está conectada.","accounts.flash.no_youtube_channels":"No se encontraron canales de YouTube. Crea un canal primero.","analytics.channels":"Canales","analytics.no_accounts":"No hay cuentas conectadas con analytics.","analytics.select_account":"Selecciona una cuenta para ver analytics.","analytics.no_data":"No hay datos de analytics disponibles.","auth.failed":"Estas credenciales no coinciden con nuestros registros.","auth.password":"La contraseña proporcionada es incorrecta.","auth.throttle":"Demasiados intentos de inicio de sesión. Inténtalo de nuevo en :seconds segundos.","auth.flash.welcome":"¡Bienvenido a TryPost!","auth.flash.welcome_trial":"¡Bienvenido a TryPost! Tu prueba ha comenzado.","auth.legal":"Al continuar, aceptas nuestros Términos de Servicio y Política de Privacidad.","auth.slides.calendar.title":"Calendario Visual","auth.slides.calendar.description":"Planifica y programa tu contenido con un calendario intuitivo de arrastrar y soltar en todas tus cuentas sociales.","auth.slides.scheduling.title":"Programación Inteligente","auth.slides.scheduling.description":"Programa posts en LinkedIn, X, Instagram, TikTok, YouTube y más — todo desde un solo lugar.","auth.slides.media.title":"Contenido Multimedia","auth.slides.media.description":"Publica imágenes, carruseles, historias y reels. Cada plataforma recibe el formato correcto automáticamente.","auth.slides.video.title":"Publicación de Video","auth.slides.video.description":"Sube videos una vez y publícalos en TikTok, YouTube Shorts, Instagram Reels y Facebook Reels.","auth.slides.team.title":"Workspaces en Equipo","auth.slides.team.description":"Invita a tu equipo, asigna roles y gestiona múltiples marcas en workspaces separados.","auth.slides.hashtags.title":"Grupos de Hashtags","auth.slides.hashtags.description":"Guarda colecciones de hashtags y agrégalos a tus posts con un clic. Organiza con etiquetas y filtros.","auth.or_continue_with":"O continuar con","auth.google_login":"Iniciar sesión con Google","auth.google_signup":"Registrarse con Google","auth.signup_success.page_title":"Bienvenido","auth.signup_success.title":"Configurando tu cuenta","auth.signup_success.description":"Esto suele tardar solo unos segundos...","auth.login.title":"Inicia sesión en tu cuenta","auth.login.description":"Introduce tu correo y contraseña para iniciar sesión","auth.login.page_title":"Iniciar sesión","auth.login.email":"Correo electrónico","auth.login.password":"Contraseña","auth.login.forgot_password":"¿Olvidaste tu contraseña?","auth.login.remember_me":"Recuérdame","auth.login.submit":"Iniciar sesión","auth.login.no_account":"¿No tienes una cuenta?","auth.login.sign_up":"Regístrate","auth.register.title":"Crear una cuenta","auth.register.description":"Introduce tus datos para crear tu cuenta","auth.register.page_title":"Registro","auth.register.name":"Nombre","auth.register.name_placeholder":"Nombre completo","auth.register.email":"Correo electrónico","auth.register.password":"Contraseña","auth.register.show_password":"Mostrar contraseña","auth.register.hide_password":"Ocultar contraseña","auth.register.submit":"Crear cuenta","auth.register.has_account":"¿Ya tienes una cuenta?","auth.register.log_in":"Iniciar sesión","auth.forgot_password.title":"Olvidé mi contraseña","auth.forgot_password.description":"Introduce tu correo para recibir un enlace de restablecimiento","auth.forgot_password.page_title":"Olvidé mi contraseña","auth.forgot_password.email":"Correo electrónico","auth.forgot_password.submit":"Enviar enlace de restablecimiento","auth.forgot_password.return_to":"O vuelve a","auth.forgot_password.log_in":"iniciar sesión","auth.reset_password.title":"Restablecer contraseña","auth.reset_password.description":"Introduce tu nueva contraseña","auth.reset_password.page_title":"Restablecer contraseña","auth.reset_password.email":"Correo electrónico","auth.reset_password.password":"Contraseña","auth.reset_password.confirm_password":"Confirmar contraseña","auth.reset_password.confirm_placeholder":"Confirmar contraseña","auth.reset_password.submit":"Restablecer contraseña","auth.verify_email.title":"Verificar correo","auth.verify_email.description":"Verifica tu correo electrónico haciendo clic en el enlace que acabamos de enviarte.","auth.verify_email.page_title":"Verificación de correo","auth.verify_email.link_sent":"Se ha enviado un nuevo enlace de verificación al correo electrónico proporcionado durante el registro.","auth.verify_email.resend":"Reenviar correo de verificación","auth.verify_email.log_out":"Cerrar sesión","auth.accept_invite.page_title":"Aceptar invitación","auth.accept_invite.title":"¡Has sido invitado!","auth.accept_invite.description":"Has sido invitado a unirte al workspace :workspace.","auth.accept_invite.workspace":"Workspace","auth.accept_invite.your_role":"Tu rol","auth.accept_invite.email":"Correo electrónico","auth.accept_invite.accept":"Aceptar invitación","auth.accept_invite.decline":"Rechazar invitación","auth.accept_invite.login_prompt":"Inicia sesión o crea una cuenta para aceptar esta invitación.","auth.accept_invite.log_in":"Iniciar sesión","auth.accept_invite.create_account":"Crear cuenta","billing.title":"Suscripción","billing.description":"Administra tu suscripción y método de pago","billing.subscribe.page_title":"Comienza tu prueba gratuita","billing.subscribe.title":"Comienza tu prueba gratuita","billing.subscribe.description":":days días gratis para explorar todas las funciones.","billing.subscribe.start_trial":"Comenzar prueba de :days días","billing.subscribe.cancel_anytime":"Cancela en cualquier momento. Sin preguntas.","billing.subscribe.switch_workspace":"Cambiar workspace","billing.subscribe.features.calendar":"Calendario visual con arrastrar y soltar","billing.subscribe.features.scheduling":"Programación ilimitada de posts","billing.subscribe.features.media":"Imágenes, carruseles e historias","billing.subscribe.features.video":"Publicación de videos en todas las plataformas","billing.subscribe.features.team":"Colaboración en equipo y workspaces","billing.subscribe.features.hashtags":"Grupos de hashtags y etiquetas","billing.trial.title":"Periodo de prueba activo","billing.trial.description":"Tu prueba termina el :date. Después, tu suscripción se cobrará automáticamente.","billing.subscription.title":"Tu suscripción","billing.subscription.status":"Estado","billing.subscription.workspaces":"Workspaces","billing.subscription.quantity":"Cantidad de suscripción","billing.subscription.expires":"Expira :date","billing.subscription.canceled_on":"Tu suscripción se cancelará el :date","billing.subscription.manage":"Administrar en Stripe","billing.invoices.title":"Facturas","billing.invoices.description":"Historial de pagos","billing.invoices.empty":"No se encontraron facturas","billing.invoices.paid":"Pagado","billing.processing.page_title":"Procesando...","billing.processing.title":"Procesando tu suscripción","billing.processing.description":"Espera mientras configuramos tu cuenta. Solo tomará un momento.","billing.processing.success_title":"¡Todo listo!","billing.processing.success_description":"Tu suscripción está activa. Redirigiendo a tus workspaces...","billing.processing.cancelled_title":"Pago cancelado","billing.processing.cancelled_description":"Tu pago fue cancelado. No se realizaron cargos.","billing.processing.retry":"Intentar de nuevo","billing.status.active":"Activa","billing.status.canceled":"Cancelada","billing.status.incomplete":"Incompleta","billing.status.incomplete_expired":"Expirada","billing.status.past_due":"Vencida","billing.status.trialing":"Prueba","billing.status.unpaid":"Sin pagar","brands.new_brand":"Nueva Marca","brands.no_brands_yet":"No hay marcas todavía","brands.no_brands_description":"Crea marcas para organizar tus cuentas de redes sociales por cliente o proyecto","brands.accounts_count":":count cuentas","brands.create.title":"Crear Marca","brands.create.description":"Dale un nombre a tu marca para agrupar cuentas de redes sociales","brands.create.name":"Nombre de la Marca","brands.create.name_placeholder":"ej. Acme Corp, Personal","brands.create.submit":"Crear Marca","brands.create.submitting":"Creando...","brands.edit.title":"Editar Marca","brands.edit.description":"Actualiza el nombre de esta marca","brands.edit.name":"Nombre de la Marca","brands.edit.name_placeholder":"ej. Acme Corp, Personal","brands.edit.submit":"Guardar Cambios","brands.edit.submitting":"Guardando...","brands.delete.title":"Eliminar Marca","brands.delete.description":"¿Estás seguro de que deseas eliminar esta marca? Las cuentas de redes sociales se desasignarán pero no se eliminarán.","brands.delete.confirm":"Eliminar","brands.delete.cancel":"Cancelar","brands.flash.created":"¡Marca creada con éxito!","brands.flash.updated":"¡Marca actualizada con éxito!","brands.flash.deleted":"¡Marca eliminada con éxito!","calendar.title":"Calendario","calendar.today":"Hoy","calendar.day":"Día","calendar.week":"Semana","calendar.month":"Mes","calendar.new_post":"Nuevo post","calendar.no_content":"Sin contenido","calendar.more":"+:count más","common.confirm_modal.cannot_be_undone":"Esta acción no se puede deshacer.","common.confirm_modal.type":"Escribe","common.confirm_modal.to_confirm":"para confirmar.","common.confirm_modal.copy_to_clipboard":"Copiar al portapapeles","common.photo_upload.upload":"Subir","common.photo_upload.uploading":"Subiendo...","common.photo_upload.remove":"Eliminar foto","common.photo_upload.hint":"Recomendado: imagen cuadrada, máximo 2 MB.","common.timezone.select":"Seleccionar zona horaria","common.timezone.search":"Buscar zona horaria...","common.timezone.empty":"No se encontró zona horaria","common.date_picker.select":"Seleccionar fecha","common.cancel":"Cancelar","hashtags.title":"Hashtags","hashtags.description":"Crea grupos de hashtags para agregarlos rápidamente a tus posts","hashtags.search":"Buscar hashtags...","hashtags.new_group":"Nuevo grupo","hashtags.no_groups_yet":"Aún no hay grupos de hashtags","hashtags.no_groups_description":"Crea grupos de hashtags para agregar rápidamente hashtags populares a tus posts","hashtags.create_first_group":"Crea tu primer grupo","hashtags.hashtags_count":":count hashtags","hashtags.create.title":"Crear grupo de hashtags","hashtags.create.description":"Dale un nombre a tu grupo y agrega hashtags separados por espacios o comas","hashtags.create.name":"Nombre del grupo","hashtags.create.name_placeholder":"ej: Marketing, Viajes, Comida","hashtags.create.hashtags":"Hashtags","hashtags.create.hashtags_placeholder":"#marketing #redessociales #negocios #crecimiento","hashtags.create.hashtags_hint":"Ingresa hashtags separados por espacios o comas. Incluye el símbolo #.","hashtags.create.submit":"Crear grupo","hashtags.create.submitting":"Creando...","hashtags.edit.title":"Editar grupo de hashtags","hashtags.edit.description":"Actualiza el nombre y los hashtags de este grupo","hashtags.edit.name":"Nombre del grupo","hashtags.edit.name_placeholder":"ej: Marketing, Viajes, Comida","hashtags.edit.hashtags":"Hashtags","hashtags.edit.hashtags_placeholder":"#marketing #redessociales #negocios #crecimiento","hashtags.edit.hashtags_hint":"Ingresa hashtags separados por espacios o comas. Incluye el símbolo #.","hashtags.edit.submit":"Guardar cambios","hashtags.edit.submitting":"Guardando...","hashtags.delete.title":"Eliminar grupo de hashtags","hashtags.delete.description":"¿Estás seguro de que deseas eliminar este grupo de hashtags? Esta acción no se puede deshacer.","hashtags.delete.confirm":"Eliminar","hashtags.delete.cancel":"Cancelar","hashtags.flash.created":"¡Grupo de hashtags creado correctamente!","hashtags.flash.updated":"¡Grupo de hashtags actualizado correctamente!","hashtags.flash.deleted":"¡Grupo de hashtags eliminado correctamente!","labels.title":"Etiquetas","labels.description":"Crea etiquetas para organizar y categorizar tus posts","labels.search":"Buscar etiquetas...","labels.new_label":"Nueva etiqueta","labels.no_labels_yet":"Aún no hay etiquetas","labels.create_first_label":"Crea tu primera etiqueta","labels.create.title":"Crear etiqueta","labels.create.description":"Dale un nombre y elige un color para tu etiqueta","labels.create.name":"Nombre","labels.create.name_placeholder":"Nombre de la etiqueta...","labels.create.color":"Color","labels.create.submit":"Crear etiqueta","labels.create.submitting":"Creando...","labels.edit.title":"Editar etiqueta","labels.edit.description":"Actualiza el nombre y el color de esta etiqueta","labels.edit.name":"Nombre","labels.edit.name_placeholder":"Nombre de la etiqueta...","labels.edit.color":"Color","labels.edit.submit":"Guardar cambios","labels.edit.submitting":"Guardando...","labels.delete.title":"Eliminar etiqueta","labels.delete.description":"¿Estás seguro de que deseas eliminar esta etiqueta? Esta acción no se puede deshacer.","labels.delete.confirm":"Eliminar","labels.delete.cancel":"Cancelar","labels.flash.created":"¡Etiqueta creada correctamente!","labels.flash.updated":"¡Etiqueta actualizada correctamente!","labels.flash.deleted":"¡Etiqueta eliminada correctamente!","mail.workspace_connections_disconnected.subject":"{1} :count cuenta necesita ser reconectada en :workspace|[2,*] :count cuentas necesitan ser reconectadas en :workspace","mail.workspace_connections_disconnected.title":"Cuentas necesitan reconexión","mail.workspace_connections_disconnected.intro":"Las siguientes cuentas sociales en tu workspace :workspace se han desconectado y necesitan ser reconectadas:","mail.workspace_connections_disconnected.reasons_title":"Esto puede haber ocurrido porque:","mail.workspace_connections_disconnected.reason_expired":"Los tokens de acceso expiraron","mail.workspace_connections_disconnected.reason_revoked":"Revocaste el acceso a TryPost en la plataforma","mail.workspace_connections_disconnected.reason_changed":"La plataforma cambió sus requisitos de autenticación","mail.workspace_connections_disconnected.reconnect_cta":"Reconecta estas cuentas para seguir programando y publicando posts.","mail.workspace_connections_disconnected.button":"Reconectar cuentas","onboarding.role.page_title":"Bienvenido - Cuéntanos sobre ti","onboarding.role.title":"Cuéntanos sobre ti","onboarding.role.description":"Ayúdanos a personalizar tu experiencia","onboarding.role.submit":"Continuar","onboarding.personas.founder.label":"Fundador","onboarding.personas.founder.description":"Construyendo una startup o nuevo negocio","onboarding.personas.creator.label":"Creador de Contenido","onboarding.personas.creator.description":"Creador de contenido o influencer","onboarding.personas.agency.label":"Agencia","onboarding.personas.agency.description":"Agencia de marketing o redes sociales","onboarding.personas.enterprise.label":"Empresa","onboarding.personas.enterprise.description":"Gran empresa o corporación","onboarding.personas.small_business.label":"Pequeño Negocio","onboarding.personas.small_business.description":"Pequeña o mediana empresa","onboarding.personas.personal.label":"Personal","onboarding.personas.personal.description":"Marca personal o hobby","onboarding.connect.page_title":"Conecta tus cuentas","onboarding.connect.title":"Conecta tus cuentas","onboarding.connect.description":"Conecta al menos una red social para comenzar","onboarding.connect.submit":"Continuar","onboarding.connect.error":"Algo salió mal. Inténtalo de nuevo.","onboarding.connect.go_back":"Volver","pagination.previous":"« Anterior","pagination.next":"Siguiente »","passwords.reset":"Tu contraseña ha sido restablecida.","passwords.sent":"Te hemos enviado un enlace para restablecer tu contraseña.","passwords.throttled":"Espera antes de intentarlo de nuevo.","passwords.token":"Este token de restablecimiento de contraseña no es válido.","passwords.user":"No encontramos un usuario con ese correo electrónico.","posts.title":"Posts","posts.search":"Buscar posts...","posts.all_posts":"Todos los posts","posts.new_post":"Nuevo post","posts.no_posts":"No se encontraron posts","posts.start_creating":"Empieza creando tu primer post.","posts.manage_posts":"Administra todos tus posts","posts.delete_confirm":"¿Estás seguro de que deseas eliminar este post?","posts.by":"por","posts.actions.view":"Ver post","posts.actions.delete":"Eliminar post","posts.form.post_type":"Tipo de post","posts.form.board":"Tablero","posts.form.select_board":"Seleccionar tablero","posts.form.search_board":"Buscar tablero...","posts.form.no_board_found":"No se encontró tablero","posts.form.media":"Multimedia","posts.form.min":"Min","posts.form.uploading":"Subiendo...","posts.form.drop_to_upload":"Suelta para subir","posts.form.drag_and_drop":"Arrastra y suelta o haz clic para subir","posts.form.photos_and_videos":"Fotos y videos","posts.form.photos_only":"Solo fotos","posts.form.videos_only":"Solo videos","posts.form.drag_to_reorder":"Arrastra para reordenar","posts.form.caption":"Descripción","posts.form.write_caption":"Escribe tu descripción...","posts.form.tiktok.settings":"Configuración de TikTok","posts.form.tiktok.privacy_level":"¿Quién puede ver este video?","posts.form.tiktok.privacy.public":"Público para todos","posts.form.tiktok.privacy.friends":"Amigos mutuos","posts.form.tiktok.privacy.followers":"Seguidores","posts.form.tiktok.privacy.private":"Solo yo","posts.form.tiktok.privacy_hint":"Las opciones disponibles dependen de la configuración de tu cuenta de TikTok.","posts.form.tiktok.auto_add_music":"Agregar música automáticamente","posts.form.tiktok.auto_add_music_hint":"Disponible solo para fotos. Agrega una música predeterminada que puedes cambiar después.","posts.form.tiktok.yes":"Sí","posts.form.tiktok.no":"No","posts.form.tiktok.allow_users":"Permitir a los usuarios:","posts.form.tiktok.comments":"Comentar","posts.form.tiktok.duet":"Dueto","posts.form.tiktok.stitch":"Stitch","posts.form.tiktok.is_aigc":"Video hecho con IA","posts.form.tiktok.brand_content":"Divulgar asociación pagada","posts.form.tiktok.brand_content_hint":"Este video promueve un negocio, marca o producto de terceros.","posts.form.tiktok.brand_organic":"Divulgar tu propia marca","posts.form.tiktok.brand_organic_hint":"Este video promueve tu propio negocio, marca o producto.","posts.status.pending":"Pendiente","posts.status.draft":"Borrador","posts.status.scheduled":"Programado","posts.status.publishing":"Publicando","posts.status.published":"Publicado","posts.status.partially_published":"Parcialmente publicado","posts.status.failed":"Fallido","posts.descriptions.draft":"Posts esperando ser programados","posts.descriptions.scheduled":"Posts programados para publicar","posts.descriptions.published":"Posts ya publicados","posts.edit.title":"Editar post","posts.edit.view_title":"Ver post","posts.edit.manage_platforms":"Administrar plataformas","posts.edit.sync":"Sincronizar","posts.edit.labels":"Etiquetas","posts.edit.hashtags":"Hashtags","posts.edit.schedule":"Programar","posts.edit.publish":"Publicar","posts.edit.delete":"Eliminar","posts.edit.settings":"Configuración","posts.edit.schedule_for":"Programar para","posts.edit.saving":"Guardando...","posts.edit.saved":"Guardado","posts.edit.scheduled_at":"Programado:","posts.edit.published_at":"Publicado:","posts.edit.media":"Multimedia","posts.edit.caption":"Descripción","posts.edit.no_caption":"Sin descripción","posts.edit.no_content":"Sin contenido","posts.edit.empty_state.title":"No hay plataformas seleccionadas","posts.edit.empty_state.description":"Selecciona al menos una plataforma para crear tu post","posts.edit.delete_modal.title":"Eliminar post","posts.edit.delete_modal.description":"¿Estás seguro de que deseas eliminar este post? Esta acción no se puede deshacer.","posts.edit.delete_modal.action":"Eliminar","posts.edit.delete_modal.cancel":"Cancelar","posts.edit.sync_enable.title":"¿Activar sincronización?","posts.edit.sync_enable.description":"Todas las plataformas compartirán el mismo contenido. Las ediciones personalizadas realizadas en plataformas individuales serán reemplazadas con el contenido actual.","posts.edit.sync_enable.cancel":"Cancelar","posts.edit.sync_enable.action":"Activar sincronización","posts.edit.sync_disable.title":"¿Desactivar sincronización?","posts.edit.sync_disable.description":"Cada plataforma mantendrá su contenido actual, pero las ediciones futuras solo se aplicarán a la plataforma que estés editando.","posts.edit.sync_disable.customize_note":"Podrás personalizar el contenido de cada plataforma individualmente.","posts.edit.sync_disable.cancel":"Cancelar","posts.edit.sync_disable.action":"Desactivar sincronización","posts.edit.platforms_dialog.title":"Seleccionar plataformas","posts.edit.platforms_dialog.description":"Elige en qué plataformas publicar este post.","posts.edit.hashtags_modal.search":"Buscar hashtags...","posts.edit.hashtags_modal.no_results":"No se encontraron hashtags.","posts.edit.validation.select_board":"Selecciona un tablero","posts.edit.validation.images_not_supported":"Imágenes no soportadas","posts.edit.validation.videos_not_supported":"Videos no soportados","posts.edit.validation.max_images":"Máximo :count imágenes","posts.edit.validation.requires_media":"Requiere multimedia","posts.edit.validation.requires_content":"Se requiere texto","posts.edit.validation.exceeded":":count excedido","posts.edit.validation.does_not_support_images":":platform no soporta imágenes","posts.edit.validation.supports_up_to_images":":platform soporta hasta :count imágenes","posts.edit.validation.does_not_support_videos":":platform no soporta videos","posts.content_types.instagram_feed.label":"Post del feed","posts.content_types.instagram_feed.description":"Aparece en tu feed y perfil","posts.content_types.instagram_reel.label":"Reel","posts.content_types.instagram_reel.description":"Video corto de hasta 90 segundos","posts.content_types.instagram_story.label":"Historia","posts.content_types.instagram_story.description":"Desaparece después de 24 horas","posts.content_types.linkedin_post.label":"Post","posts.content_types.linkedin_post.description":"Post estándar con texto y multimedia","posts.content_types.linkedin_carousel.label":"Carrusel","posts.content_types.linkedin_carousel.description":"Imágenes deslizables","posts.content_types.linkedin_page_post.label":"Post","posts.content_types.linkedin_page_post.description":"Post estándar con texto y multimedia","posts.content_types.linkedin_page_carousel.label":"Carrusel","posts.content_types.linkedin_page_carousel.description":"Imágenes deslizables","posts.content_types.facebook_post.label":"Post","posts.content_types.facebook_post.description":"Post estándar en tu página","posts.content_types.facebook_reel.label":"Reel","posts.content_types.facebook_reel.description":"Video corto de hasta 90 segundos","posts.content_types.facebook_story.label":"Historia","posts.content_types.facebook_story.description":"Desaparece después de 24 horas","posts.content_types.tiktok_video.label":"Video","posts.content_types.tiktok_video.description":"Contenido de video corto","posts.content_types.youtube_short.label":"Short","posts.content_types.youtube_short.description":"Video vertical de hasta 60 segundos","posts.content_types.x_post.label":"Post","posts.content_types.x_post.description":"Tweet con texto y multimedia","posts.content_types.threads_post.label":"Post","posts.content_types.threads_post.description":"Post de texto con multimedia opcional","posts.content_types.pinterest_pin.label":"Pin","posts.content_types.pinterest_pin.description":"Pin de imagen con enlace","posts.content_types.pinterest_video_pin.label":"Pin de video","posts.content_types.pinterest_video_pin.description":"Contenido de video","posts.content_types.pinterest_carousel.label":"Carrusel","posts.content_types.pinterest_carousel.description":"2-5 imágenes","posts.content_types.bluesky_post.label":"Post","posts.content_types.bluesky_post.description":"Post de texto con imágenes opcionales","posts.content_types.mastodon_post.label":"Post","posts.content_types.mastodon_post.description":"Post de texto con multimedia opcional","posts.platforms.linkedin":"LinkedIn","posts.platforms.linkedin-page":"Página de LinkedIn","posts.platforms.x":"X","posts.platforms.tiktok":"TikTok","posts.platforms.youtube":"YouTube Shorts","posts.platforms.facebook":"Página de 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 programado correctamente!","posts.flash.publishing":"¡El post se está publicando!","posts.flash.deleted":"¡Post eliminado correctamente!","posts.flash.cannot_edit_published":"Los posts publicados no se pueden editar.","posts.flash.connect_first":"Conecta al menos una red social antes de crear un post.","posts.errors.account_disconnected":"Cuenta social desconectada","posts.errors.account_inactive":"Cuenta social desactivada","settings.title":"Configuración","settings.description":"Administra tu perfil y configuración de la cuenta","settings.nav.profile":"Perfil","settings.nav.password":"Contraseña","settings.nav.workspace":"Workspace","settings.nav.members":"Miembros","settings.nav.notifications":"Notificaciones","settings.nav.billing":"Facturación","settings.notifications.title":"Preferencias de notificaciones","settings.notifications.heading":"Notificaciones por correo","settings.notifications.description":"Elige qué notificaciones por correo deseas recibir","settings.notifications.post_published":"Post publicado","settings.notifications.post_published_description":"Recibir un correo cuando tu post se publique correctamente","settings.notifications.post_failed":"Post fallido","settings.notifications.post_failed_description":"Recibir un correo cuando tu post falle al publicar","settings.notifications.account_disconnected":"Cuenta desconectada","settings.notifications.account_disconnected_description":"Recibir un correo cuando una cuenta social se desconecte","settings.notifications.save":"Guardar preferencias","settings.profile.title":"Configuración del perfil","settings.profile.photo_heading":"Foto de perfil","settings.profile.photo_description":"Sube una foto de perfil","settings.profile.heading":"Información del perfil","settings.profile.description":"Actualiza tu nombre y correo electrónico","settings.profile.avatar":"Avatar","settings.profile.name":"Nombre","settings.profile.name_placeholder":"Nombre completo","settings.profile.email":"Correo electrónico","settings.profile.email_placeholder":"Correo electrónico","settings.profile.email_unverified":"Tu correo electrónico no ha sido verificado.","settings.profile.resend_verification":"Haz clic aquí para reenviar el correo de verificación.","settings.profile.verification_sent":"Se ha enviado un nuevo enlace de verificación a tu correo electrónico.","settings.profile.save":"Guardar","settings.password.title":"Configuración de contraseña","settings.password.heading":"Actualizar contraseña","settings.password.description":"Asegúrate de que tu cuenta use una contraseña larga y aleatoria para mantenerte seguro","settings.password.current_password":"Contraseña actual","settings.password.current_password_placeholder":"Contraseña actual","settings.password.new_password":"Nueva contraseña","settings.password.new_password_placeholder":"Nueva contraseña","settings.password.confirm_password":"Confirmar contraseña","settings.password.confirm_password_placeholder":"Confirmar contraseña","settings.password.save":"Guardar contraseña","settings.delete_account.heading":"Eliminar cuenta","settings.delete_account.description":"Elimina tu cuenta y todos sus recursos","settings.delete_account.warning":"Advertencia","settings.delete_account.warning_message":"Procede con precaución, esta acción no se puede deshacer.","settings.delete_account.button":"Eliminar cuenta","settings.delete_account.modal_title":"¿Estás seguro de que deseas eliminar tu cuenta?","settings.delete_account.modal_description":"Una vez eliminada tu cuenta, todos sus recursos y datos también se eliminarán permanentemente. Introduce tu contraseña para confirmar que deseas eliminar permanentemente tu cuenta.","settings.delete_account.password":"Contraseña","settings.delete_account.password_placeholder":"Contraseña","settings.delete_account.cancel":"Cancelar","settings.delete_account.confirm":"Eliminar cuenta","settings.workspace.title":"Configuración del workspace","settings.workspace.logo_heading":"Logo del workspace","settings.workspace.logo_description":"Sube un logo para tu workspace","settings.workspace.heading":"Nombre del workspace","settings.workspace.description":"Actualiza el nombre y zona horaria del workspace","settings.workspace.members_heading":"Miembros","settings.workspace.members_description":"Administra miembros e invitaciones del workspace","settings.workspace.name":"Nombre","settings.workspace.name_placeholder":"Mi Workspace","settings.workspace.timezone":"Zona horaria","settings.workspace.save":"Guardar","settings.members.title":"Miembros","settings.members.heading":"Miembros del equipo","settings.members.description":"Administra miembros e invitaciones de este workspace","settings.members.cancel":"Cancelar","settings.members.remove":"Eliminar","settings.members.make_admin":"Hacer administrador","settings.members.make_member":"Hacer miembro","settings.members.invite.title":"Invitar miembro","settings.members.invite.description":"Envía una invitación por correo para agregar colaboradores","settings.members.invite.email":"Correo electrónico","settings.members.invite.email_placeholder":"colaborador@email.com","settings.members.invite.role":"Rol","settings.members.invite.role_placeholder":"Selecciona un rol","settings.members.invite.submit":"Enviar invitación","settings.members.pending.title":"Invitaciones pendientes","settings.members.pending.description":"Invitaciones en espera de aceptación","settings.members.pending.empty":"No hay invitaciones pendientes","settings.members.list.title":"Miembros","settings.members.list.description":"Personas con acceso a este workspace","settings.members.list.empty":"No hay miembros además del propietario","settings.members.remove_modal.title":"Eliminar miembro","settings.members.remove_modal.description":"¿Estás seguro de que deseas eliminar a este miembro del workspace? Perderá acceso a todos los recursos del workspace.","settings.members.remove_modal.action":"Eliminar miembro","settings.members.cancel_invite_modal.title":"Cancelar invitación","settings.members.cancel_invite_modal.description":"¿Estás seguro de que deseas cancelar esta invitación?","settings.members.cancel_invite_modal.action":"Cancelar invitación","settings.members.roles.owner":"Propietario","settings.members.roles.admin":"Administrador","settings.members.roles.member":"Miembro","settings.members.flash.invite_sent":"¡Invitación enviada correctamente!","settings.members.flash.invite_deleted":"Invitación eliminada.","settings.members.flash.member_removed":"¡Miembro eliminado correctamente!","settings.members.flash.role_updated":"Rol del miembro actualizado.","settings.members.flash.wrong_email":"Esta invitación es para otro correo electrónico.","settings.members.flash.already_member":"Ya eres miembro de este workspace.","settings.members.flash.invite_accepted":"¡Bienvenido! Ahora eres miembro del workspace.","settings.members.flash.invite_declined":"Invitación rechazada.","settings.flash.profile_updated":"¡Perfil actualizado correctamente!","settings.flash.language_updated":"¡Idioma actualizado correctamente!","settings.flash.password_updated":"¡Contraseña actualizada correctamente!","settings.flash.workspace_updated":"¡Configuración actualizada correctamente!","settings.flash.photo_updated":"¡Foto actualizada correctamente!","settings.flash.photo_deleted":"¡Foto eliminada correctamente!","settings.flash.logo_updated":"¡Logo subido correctamente!","settings.flash.logo_deleted":"¡Logo eliminado correctamente!","settings.flash.notifications_updated":"¡Preferencias de notificaciones actualizadas!","settings.api_keys.title":"Claves API","settings.api_keys.page_title":"Claves API","settings.api_keys.heading":"Claves API","settings.api_keys.description":"Administra claves API para acceso programático a tu workspace.","settings.api_keys.create":"Crear clave API","settings.api_keys.copy":"Copiar","settings.api_keys.new_token_message":"Tu nueva clave API ha sido creada. Cópiala ahora — no podrás verla de nuevo.","settings.api_keys.table.name":"Nombre","settings.api_keys.table.key":"Clave","settings.api_keys.table.status":"Estado","settings.api_keys.table.expires":"Expira","settings.api_keys.table.last_used":"Último uso","settings.api_keys.table.never":"Nunca","settings.api_keys.actions.copy_id":"Copiar ID de clave API","settings.api_keys.actions.copy_id_success":"ID de clave API copiado","settings.api_keys.actions.delete":"Eliminar","settings.api_keys.empty.title":"No hay claves API","settings.api_keys.empty.description":"Crea una clave API para acceder a tu workspace programáticamente.","settings.api_keys.delete_modal.title":"Eliminar clave API","settings.api_keys.delete_modal.description":"¿Estás seguro de que deseas eliminar esta clave API? Las aplicaciones que la usen perderán acceso inmediatamente.","settings.api_keys.delete_modal.action":"Eliminar clave API","settings.api_keys.create_dialog.title":"Crear clave API","settings.api_keys.create_dialog.description":"Crea una nueva clave API para acceso programático a tu workspace.","settings.api_keys.create_dialog.name":"Nombre","settings.api_keys.create_dialog.name_placeholder":"ej. Clave API de Producción","settings.api_keys.create_dialog.expires":"Fecha de expiración (opcional)","settings.api_keys.create_dialog.expires_placeholder":"Sin expiración","settings.api_keys.create_dialog.submit":"Crear","settings.api_keys.create_dialog.cancel":"Cancelar","settings.api_keys.flash.created":"¡Clave API creada correctamente!","settings.api_keys.flash.deleted":"¡Clave API eliminada correctamente!","sidebar.workspaces":"Workspaces","sidebar.select_workspace":"Seleccionar workspace","sidebar.create_workspace":"Crear workspace","sidebar.create_post":"Crear post","sidebar.profile":"Perfil","sidebar.log_out":"Cerrar sesión","sidebar.workspace.connections":"Conexiones","sidebar.workspace.hashtags":"Hashtags","sidebar.workspace.labels":"Etiquetas","sidebar.workspace.api_keys":"API Keys","sidebar.workspace.settings":"Configuración","sidebar.workspace_select":"Workspace: Seleccionar","sidebar.theme":"Tema: :name","sidebar.theme_light":"Claro","sidebar.theme_dark":"Oscuro","sidebar.theme_system":"Sistema","sidebar.language":"Idioma: :name","sidebar.language_select":"Idioma: Seleccionar","sidebar.groups.posts":"Posts","sidebar.groups.workspace":"Workspace","sidebar.groups.account":"Cuenta","sidebar.groups.support":"Soporte","sidebar.analytics":"Analytics","sidebar.posts.calendar":"Calendario","sidebar.posts.all":"Todos","sidebar.posts.scheduled":"Programados","sidebar.posts.posted":"Publicados","sidebar.posts.drafts":"Borradores","sidebar.account.settings":"Configuración","sidebar.account.usage":"Uso","sidebar.account.billing":"Facturación","sidebar.notifications":"Notificaciones","sidebar.mark_all_read":"Marcar todo como leído","sidebar.mark_as_read":"Marcar como leído","sidebar.archive_all":"Archivar todo","sidebar.no_notifications":"Sin notificaciones","sidebar.support.discord":"Discord","sidebar.support.share_feedback":"Dar feedback","sidebar.support.last_updates":"Últimas actualizaciones","sidebar.support.docs":"Documentación","usage.title":"Uso","usage.section_account":"Cuenta","usage.section_account_description":"Cuotas y límites de tu plan :plan.","usage.section_ai":"Generación AI","usage.section_ai_description":"Uso de generación de imágenes y videos AI del mes actual.","usage.section_data":"Datos","usage.section_data_description":"Retención de datos y almacenamiento de tu plan.","usage.workspaces":"Workspaces","usage.social_accounts":"Cuentas Sociales","usage.members":"Miembros","usage.ai_images":"Imágenes","usage.ai_videos":"Videos","usage.data_retention":"Retención de Datos","usage.unlimited":"Ilimitado","usage.days":"días","usage.year":"año","usage.years":"años","validation.accepted":"El campo :attribute debe ser aceptado.","validation.accepted_if":"El campo :attribute debe ser aceptado cuando :other es :value.","validation.active_url":"El campo :attribute debe ser una URL válida.","validation.after":"El campo :attribute debe ser una fecha posterior a :date.","validation.after_or_equal":"El campo :attribute debe ser una fecha posterior o igual a :date.","validation.alpha":"El campo :attribute solo puede contener letras.","validation.alpha_dash":"El campo :attribute solo puede contener letras, números, guiones y guiones bajos.","validation.alpha_num":"El campo :attribute solo puede contener letras y números.","validation.any_of":"El campo :attribute no es válido.","validation.array":"El campo :attribute debe ser un arreglo.","validation.ascii":"El campo :attribute solo puede contener caracteres alfanuméricos de un byte y símbolos.","validation.before":"El campo :attribute debe ser una fecha anterior a :date.","validation.before_or_equal":"El campo :attribute debe ser una fecha anterior o igual a :date.","validation.between.array":"El campo :attribute debe tener entre :min y :max elementos.","validation.between.file":"El campo :attribute debe pesar entre :min y :max kilobytes.","validation.between.numeric":"El campo :attribute debe estar entre :min y :max.","validation.between.string":"El campo :attribute debe tener entre :min y :max caracteres.","validation.boolean":"El campo :attribute debe ser verdadero o falso.","validation.can":"El campo :attribute contiene un valor no autorizado.","validation.confirmed":"La confirmación del campo :attribute no coincide.","validation.contains":"Al campo :attribute le falta un valor requerido.","validation.current_password":"La contraseña es incorrecta.","validation.date":"El campo :attribute debe ser una fecha válida.","validation.date_equals":"El campo :attribute debe ser una fecha igual a :date.","validation.date_format":"El campo :attribute debe coincidir con el formato :format.","validation.decimal":"El campo :attribute debe tener :decimal decimales.","validation.declined":"El campo :attribute debe ser rechazado.","validation.declined_if":"El campo :attribute debe ser rechazado cuando :other es :value.","validation.different":"El campo :attribute y :other deben ser diferentes.","validation.digits":"El campo :attribute debe tener :digits dígitos.","validation.digits_between":"El campo :attribute debe tener entre :min y :max dígitos.","validation.dimensions":"El campo :attribute tiene dimensiones de imagen no válidas.","validation.distinct":"El campo :attribute tiene un valor duplicado.","validation.doesnt_contain":"El campo :attribute no debe contener ninguno de los siguientes: :values.","validation.doesnt_end_with":"El campo :attribute no debe terminar con uno de los siguientes: :values.","validation.doesnt_start_with":"El campo :attribute no debe comenzar con uno de los siguientes: :values.","validation.email":"El campo :attribute debe ser un correo electrónico válido.","validation.encoding":"El campo :attribute debe estar codificado en :encoding.","validation.ends_with":"El campo :attribute debe terminar con uno de los siguientes: :values.","validation.enum":"El :attribute seleccionado no es válido.","validation.exists":"El :attribute seleccionado no es válido.","validation.extensions":"El campo :attribute debe tener una de las siguientes extensiones: :values.","validation.file":"El campo :attribute debe ser un archivo.","validation.filled":"El campo :attribute debe tener un valor.","validation.gt.array":"El campo :attribute debe tener más de :value elementos.","validation.gt.file":"El campo :attribute debe pesar más de :value kilobytes.","validation.gt.numeric":"El campo :attribute debe ser mayor que :value.","validation.gt.string":"El campo :attribute debe tener más de :value caracteres.","validation.gte.array":"El campo :attribute debe tener :value elementos o más.","validation.gte.file":"El campo :attribute debe pesar :value kilobytes o más.","validation.gte.numeric":"El campo :attribute debe ser mayor o igual a :value.","validation.gte.string":"El campo :attribute debe tener :value caracteres o más.","validation.hex_color":"El campo :attribute debe ser un color hexadecimal válido.","validation.image":"El campo :attribute debe ser una imagen.","validation.in":"El :attribute seleccionado no es válido.","validation.in_array":"El campo :attribute debe existir en :other.","validation.in_array_keys":"El campo :attribute debe contener al menos una de las siguientes claves: :values.","validation.integer":"El campo :attribute debe ser un número entero.","validation.ip":"El campo :attribute debe ser una dirección IP válida.","validation.ipv4":"El campo :attribute debe ser una dirección IPv4 válida.","validation.ipv6":"El campo :attribute debe ser una dirección IPv6 válida.","validation.json":"El campo :attribute debe ser una cadena JSON válida.","validation.list":"El campo :attribute debe ser una lista.","validation.lowercase":"El campo :attribute debe estar en minúsculas.","validation.lt.array":"El campo :attribute debe tener menos de :value elementos.","validation.lt.file":"El campo :attribute debe pesar menos de :value kilobytes.","validation.lt.numeric":"El campo :attribute debe ser menor que :value.","validation.lt.string":"El campo :attribute debe tener menos de :value caracteres.","validation.lte.array":"El campo :attribute no debe tener más de :value elementos.","validation.lte.file":"El campo :attribute debe pesar :value kilobytes o menos.","validation.lte.numeric":"El campo :attribute debe ser menor o igual a :value.","validation.lte.string":"El campo :attribute debe tener :value caracteres o menos.","validation.mac_address":"El campo :attribute debe ser una dirección MAC válida.","validation.max.array":"El campo :attribute no debe tener más de :max elementos.","validation.max.file":"El campo :attribute no debe pesar más de :max kilobytes.","validation.max.numeric":"El campo :attribute no debe ser mayor que :max.","validation.max.string":"El campo :attribute no debe tener más de :max caracteres.","validation.max_digits":"El campo :attribute no debe tener más de :max dígitos.","validation.mimes":"El campo :attribute debe ser un archivo de tipo: :values.","validation.mimetypes":"El campo :attribute debe ser un archivo de tipo: :values.","validation.min.array":"El campo :attribute debe tener al menos :min elementos.","validation.min.file":"El campo :attribute debe pesar al menos :min kilobytes.","validation.min.numeric":"El campo :attribute debe ser al menos :min.","validation.min.string":"El campo :attribute debe tener al menos :min caracteres.","validation.min_digits":"El campo :attribute debe tener al menos :min dígitos.","validation.missing":"El campo :attribute debe estar ausente.","validation.missing_if":"El campo :attribute debe estar ausente cuando :other es :value.","validation.missing_unless":"El campo :attribute debe estar ausente a menos que :other sea :value.","validation.missing_with":"El campo :attribute debe estar ausente cuando :values está presente.","validation.missing_with_all":"El campo :attribute debe estar ausente cuando :values están presentes.","validation.multiple_of":"El campo :attribute debe ser múltiplo de :value.","validation.not_in":"El :attribute seleccionado no es válido.","validation.not_regex":"El formato del campo :attribute no es válido.","validation.numeric":"El campo :attribute debe ser un número.","validation.password.letters":"El campo :attribute debe contener al menos una letra.","validation.password.mixed":"El campo :attribute debe contener al menos una letra mayúscula y una minúscula.","validation.password.numbers":"El campo :attribute debe contener al menos un número.","validation.password.symbols":"El campo :attribute debe contener al menos un símbolo.","validation.password.uncompromised":"El :attribute proporcionado ha aparecido en una filtración de datos. Elige un :attribute diferente.","validation.present":"El campo :attribute debe estar presente.","validation.present_if":"El campo :attribute debe estar presente cuando :other es :value.","validation.present_unless":"El campo :attribute debe estar presente a menos que :other sea :value.","validation.present_with":"El campo :attribute debe estar presente cuando :values está presente.","validation.present_with_all":"El campo :attribute debe estar presente cuando :values están presentes.","validation.prohibited":"El campo :attribute está prohibido.","validation.prohibited_if":"El campo :attribute está prohibido cuando :other es :value.","validation.prohibited_if_accepted":"El campo :attribute está prohibido cuando :other es aceptado.","validation.prohibited_if_declined":"El campo :attribute está prohibido cuando :other es rechazado.","validation.prohibited_unless":"El campo :attribute está prohibido a menos que :other esté en :values.","validation.prohibits":"El campo :attribute prohíbe que :other esté presente.","validation.regex":"El formato del campo :attribute no es válido.","validation.required":"El campo :attribute es obligatorio.","validation.required_array_keys":"El campo :attribute debe contener entradas para: :values.","validation.required_if":"El campo :attribute es obligatorio cuando :other es :value.","validation.required_if_accepted":"El campo :attribute es obligatorio cuando :other es aceptado.","validation.required_if_declined":"El campo :attribute es obligatorio cuando :other es rechazado.","validation.required_unless":"El campo :attribute es obligatorio a menos que :other esté en :values.","validation.required_with":"El campo :attribute es obligatorio cuando :values está presente.","validation.required_with_all":"El campo :attribute es obligatorio cuando :values están presentes.","validation.required_without":"El campo :attribute es obligatorio cuando :values no está presente.","validation.required_without_all":"El campo :attribute es obligatorio cuando ninguno de :values está presente.","validation.same":"El campo :attribute debe coincidir con :other.","validation.size.array":"El campo :attribute debe contener :size elementos.","validation.size.file":"El campo :attribute debe pesar :size kilobytes.","validation.size.numeric":"El campo :attribute debe ser :size.","validation.size.string":"El campo :attribute debe tener :size caracteres.","validation.starts_with":"El campo :attribute debe comenzar con uno de los siguientes: :values.","validation.string":"El campo :attribute debe ser una cadena de texto.","validation.timezone":"El campo :attribute debe ser una zona horaria válida.","validation.unique":"El :attribute ya ha sido registrado.","validation.uploaded":"El :attribute no se pudo subir.","validation.uppercase":"El campo :attribute debe estar en mayúsculas.","validation.url":"El campo :attribute debe ser una URL válida.","validation.ulid":"El campo :attribute debe ser un ULID válido.","validation.uuid":"El campo :attribute debe ser un UUID válido.","validation.custom.attribute-name.rule-name":"custom-message","workspaces.title":"Workspaces","workspaces.select_title":"Tus workspaces","workspaces.select_description":"Selecciona un workspace para continuar","workspaces.current":"Actual","workspaces.connections":":count conexiones","workspaces.posts":":count posts","workspaces.create.title":"Crear un workspace","workspaces.create.description":"Dale un nombre a tu workspace para comenzar","workspaces.create.name":"Nombre del workspace","workspaces.create.name_placeholder":"Mi Workspace","workspaces.create.submit":"Crear workspace"} \ No newline at end of file diff --git a/lang/php_pt-br.json b/lang/php_pt-br.json deleted file mode 100644 index 457e28a8..00000000 --- a/lang/php_pt-br.json +++ /dev/null @@ -1 +0,0 @@ -{"accounts.title":"Conexões","accounts.page_title":"Contas Sociais","accounts.description":"Visão geral de todas as suas contas sociais conectadas","accounts.add_social":"Adicionar Rede Social","accounts.add_social_title":"Conectar uma Conta Social","accounts.add_social_description":"Conecte uma conta social ao TryPost para começar a publicar","accounts.no_accounts":"Nenhuma conta conectada ainda","accounts.no_accounts_description":"Conecte suas redes sociais para começar a agendar e publicar posts","accounts.added":"Adicionada :date","accounts.limit_reached":"Você atingiu o limite de contas sociais do seu plano.","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.descriptions.linkedin":"Conecte seu perfil pessoal do LinkedIn","accounts.descriptions.linkedin-page":"Conecte uma página de empresa do LinkedIn","accounts.descriptions.x":"Conecte sua conta do X (Twitter)","accounts.descriptions.tiktok":"Conecte sua conta do TikTok","accounts.descriptions.youtube":"Conecte um canal do YouTube","accounts.descriptions.facebook":"Conecte uma página do Facebook","accounts.descriptions.instagram":"Conecte uma conta profissional do Instagram","accounts.descriptions.instagram-facebook":"Conecte Instagram via página do Facebook","accounts.descriptions.threads":"Conecte sua conta do Threads","accounts.descriptions.pinterest":"Conecte sua conta do Pinterest","accounts.descriptions.bluesky":"Conecte sua conta do Bluesky","accounts.descriptions.mastodon":"Conecte sua conta do Mastodon","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.instagram_facebook.title":"Selecionar Conta do Instagram","accounts.instagram_facebook.description":"Escolha qual conta do Instagram você deseja conectar","accounts.instagram_facebook.no_pages":"Nenhuma conta do Instagram encontrada","accounts.instagram_facebook.no_pages_description":"Nenhuma Página do Facebook com conta Instagram Business vinculada foi encontrada.","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.activated":"Conta ativada!","accounts.flash.deactivated":"Conta desativada!","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.","analytics.channels":"Canais","analytics.no_accounts":"Nenhuma conta conectada com analytics.","analytics.select_account":"Selecione uma conta para ver analytics.","analytics.no_data":"Nenhum dado de analytics disponível.","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.legal":"Ao continuar, você concorda com nossos Termos de Serviço e Política de Privacidade.","auth.slides.calendar.title":"Calendário Visual","auth.slides.calendar.description":"Planeje e agende seu conteúdo com um calendário intuitivo de arrastar e soltar em todas as suas contas sociais.","auth.slides.scheduling.title":"Agendamento Inteligente","auth.slides.scheduling.description":"Agende posts no LinkedIn, X, Instagram, TikTok, YouTube e mais — tudo em um só lugar.","auth.slides.media.title":"Mídia Rica","auth.slides.media.description":"Publique imagens, carrosséis, stories e reels. Cada plataforma recebe o formato correto automaticamente.","auth.slides.video.title":"Publicação de Vídeo","auth.slides.video.description":"Envie vídeos uma vez e publique no TikTok, YouTube Shorts, Instagram Reels e Facebook Reels.","auth.slides.team.title":"Workspaces em Equipe","auth.slides.team.description":"Convide sua equipe, atribua funções e gerencie múltiplas marcas em workspaces separados.","auth.slides.hashtags.title":"Grupos de Hashtags","auth.slides.hashtags.description":"Salve coleções de hashtags e adicione aos posts com um clique. Organize com etiquetas e filtros.","auth.or_continue_with":"Ou continue com","auth.google_login":"Entrar com Google","auth.google_signup":"Cadastrar com Google","auth.signup_success.page_title":"Bem-vindo","auth.signup_success.title":"Configurando sua conta","auth.signup_success.description":"Isso geralmente leva apenas alguns segundos...","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.show_password":"Mostrar senha","auth.register.hide_password":"Esconder 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.subscribe.page_title":"Comece seu teste gratuito","billing.subscribe.title":"Comece seu teste gratuito","billing.subscribe.description":":days dias grátis para explorar todos os recursos.","billing.subscribe.start_trial":"Começar teste de :days dias","billing.subscribe.cancel_anytime":"Cancele a qualquer momento. Sem perguntas.","billing.subscribe.switch_workspace":"Trocar workspace","billing.subscribe.features.calendar":"Calendário visual com arrastar e soltar","billing.subscribe.features.scheduling":"Agendamento ilimitado de posts","billing.subscribe.features.media":"Imagens, carrosséis e stories","billing.subscribe.features.video":"Publicação de vídeos em todas as plataformas","billing.subscribe.features.team":"Colaboração em equipe e workspaces","billing.subscribe.features.hashtags":"Grupos de hashtags e etiquetas","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.status":"Status","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.processing.page_title":"Processando...","billing.processing.title":"Processando sua assinatura","billing.processing.description":"Aguarde enquanto configuramos sua conta. Isso levará apenas um momento.","billing.processing.success_title":"Tudo pronto!","billing.processing.success_description":"Sua assinatura está ativa. Redirecionando para seus workspaces...","billing.processing.cancelled_title":"Pagamento cancelado","billing.processing.cancelled_description":"Seu pagamento foi cancelado. Nenhuma cobrança foi realizada.","billing.processing.retry":"Tentar novamente","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","brands.new_brand":"Nova Marca","brands.no_brands_yet":"Nenhuma marca ainda","brands.no_brands_description":"Crie marcas para organizar suas contas de redes sociais por cliente ou projeto","brands.accounts_count":":count contas","brands.create.title":"Criar Marca","brands.create.description":"Dê um nome à sua marca para agrupar contas de redes sociais","brands.create.name":"Nome da Marca","brands.create.name_placeholder":"ex. Acme Corp, Pessoal","brands.create.submit":"Criar Marca","brands.create.submitting":"Criando...","brands.edit.title":"Editar Marca","brands.edit.description":"Atualize o nome desta marca","brands.edit.name":"Nome da Marca","brands.edit.name_placeholder":"ex. Acme Corp, Pessoal","brands.edit.submit":"Salvar Alterações","brands.edit.submitting":"Salvando...","brands.delete.title":"Excluir Marca","brands.delete.description":"Tem certeza de que deseja excluir esta marca? As contas de redes sociais serão desvinculadas mas não excluídas.","brands.delete.confirm":"Excluir","brands.delete.cancel":"Cancelar","brands.flash.created":"Marca criada com sucesso!","brands.flash.updated":"Marca atualizada com sucesso!","brands.flash.deleted":"Marca excluída com sucesso!","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","common.confirm_modal.cannot_be_undone":"Esta ação não pode ser desfeita.","common.confirm_modal.type":"Digite","common.confirm_modal.to_confirm":"para confirmar.","common.confirm_modal.copy_to_clipboard":"Copiar para a área de transferência","common.photo_upload.upload":"Enviar","common.photo_upload.uploading":"Enviando...","common.photo_upload.remove":"Remover foto","common.photo_upload.hint":"Recomendado: imagem quadrada, máximo 2 MB.","common.timezone.select":"Selecionar fuso horário","common.timezone.search":"Buscar fuso horário...","common.timezone.empty":"Fuso horário não encontrado","common.date_picker.select":"Selecionar data","common.cancel":"Cancelar","hashtags.title":"Hashtags","hashtags.description":"Crie grupos de hashtags para adicionar rapidamente aos seus posts","hashtags.search":"Buscar hashtags...","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.search":"Buscar etiquetas...","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","onboarding.role.page_title":"Bem-vindo - Conte-nos sobre você","onboarding.role.title":"Conte-nos sobre você","onboarding.role.description":"Nos ajude a personalizar sua experiência","onboarding.role.submit":"Continuar","onboarding.personas.founder.label":"Fundador","onboarding.personas.founder.description":"Construindo uma startup ou novo negócio","onboarding.personas.creator.label":"Criador de Conteúdo","onboarding.personas.creator.description":"Criador de conteúdo ou influenciador","onboarding.personas.agency.label":"Agência","onboarding.personas.agency.description":"Agência de marketing ou mídias sociais","onboarding.personas.enterprise.label":"Empresa","onboarding.personas.enterprise.description":"Grande empresa ou corporação","onboarding.personas.small_business.label":"Pequeno Negócio","onboarding.personas.small_business.description":"Pequena ou média empresa","onboarding.personas.personal.label":"Pessoal","onboarding.personas.personal.description":"Marca pessoal ou hobby","onboarding.connect.page_title":"Conecte suas contas","onboarding.connect.title":"Conecte suas contas","onboarding.connect.description":"Conecte pelo menos uma rede social para começar","onboarding.connect.submit":"Continuar","onboarding.connect.error":"Algo deu errado. Tente novamente.","onboarding.connect.go_back":"Voltar","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.search":"Buscar 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.form.tiktok.settings":"Configurações do TikTok","posts.form.tiktok.privacy_level":"Quem pode ver este vídeo?","posts.form.tiktok.privacy.public":"Público para todos","posts.form.tiktok.privacy.friends":"Amigos em comum","posts.form.tiktok.privacy.followers":"Seguidores","posts.form.tiktok.privacy.private":"Apenas eu","posts.form.tiktok.privacy_hint":"As opções disponíveis dependem das configurações da sua conta TikTok.","posts.form.tiktok.auto_add_music":"Adicionar música automaticamente","posts.form.tiktok.auto_add_music_hint":"Disponível apenas para fotos. Adiciona uma música padrão que pode ser alterada depois.","posts.form.tiktok.yes":"Sim","posts.form.tiktok.no":"Não","posts.form.tiktok.allow_users":"Permitir que usuários:","posts.form.tiktok.comments":"Comentem","posts.form.tiktok.duet":"Dueto","posts.form.tiktok.stitch":"Stitch","posts.form.tiktok.is_aigc":"Vídeo feito com IA","posts.form.tiktok.brand_content":"Divulgar parceria paga","posts.form.tiktok.brand_content_hint":"Este vídeo promove um negócio, marca ou produto de terceiros.","posts.form.tiktok.brand_organic":"Divulgar sua própria marca","posts.form.tiktok.brand_organic_hint":"Este vídeo promove seu próprio negócio, marca ou produto.","posts.status.pending":"Pendente","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":"Etiqueta","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.requires_content":"Texto é obrigatório","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","posts.errors.account_inactive":"Conta social está desativada","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.notifications":"Notificações","settings.nav.billing":"Faturamento","settings.notifications.title":"Preferências de notificações","settings.notifications.heading":"Notificações por e-mail","settings.notifications.description":"Escolha quais notificações por e-mail deseja receber","settings.notifications.post_published":"Post publicado","settings.notifications.post_published_description":"Receber um e-mail quando seu post for publicado com sucesso","settings.notifications.post_failed":"Post falhou","settings.notifications.post_failed_description":"Receber um e-mail quando seu post falhar ao publicar","settings.notifications.account_disconnected":"Conta desconectada","settings.notifications.account_disconnected_description":"Receber um e-mail quando uma conta social for desconectada","settings.notifications.save":"Salvar preferências","settings.profile.title":"Configurações do perfil","settings.profile.photo_heading":"Foto do perfil","settings.profile.photo_description":"Envie uma foto de 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.logo_heading":"Logo do workspace","settings.workspace.logo_description":"Envie um logo para o workspace","settings.workspace.heading":"Nome do workspace","settings.workspace.description":"Atualize o nome e fuso horário do workspace","settings.workspace.members_heading":"Membros","settings.workspace.members_description":"Gerencie membros e convites do workspace","settings.workspace.name":"Nome","settings.workspace.name_placeholder":"Meu Workspace","settings.workspace.timezone":"Fuso horário","settings.workspace.save":"Salvar","settings.members.title":"Membros","settings.members.heading":"Membros da equipe","settings.members.description":"Gerencie membros e convites deste workspace","settings.members.cancel":"Cancelar","settings.members.remove":"Remover","settings.members.make_admin":"Tornar administrador","settings.members.make_member":"Tornar membro","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.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.remove_modal.title":"Remover membro","settings.members.remove_modal.description":"Tem certeza que deseja remover este membro do workspace? Ele perderá acesso a todos os recursos do workspace.","settings.members.remove_modal.action":"Remover membro","settings.members.cancel_invite_modal.title":"Cancelar convite","settings.members.cancel_invite_modal.description":"Tem certeza que deseja cancelar este convite?","settings.members.cancel_invite_modal.action":"Cancelar convite","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.role_updated":"Função do membro atualizada.","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!","settings.flash.photo_updated":"Foto atualizada com sucesso!","settings.flash.photo_deleted":"Foto removida com sucesso!","settings.flash.logo_updated":"Logo enviado com sucesso!","settings.flash.logo_deleted":"Logo removido com sucesso!","settings.flash.notifications_updated":"Preferências de notificações atualizadas!","settings.api_keys.title":"Chaves API","settings.api_keys.page_title":"Chaves API","settings.api_keys.heading":"Chaves API","settings.api_keys.description":"Gerencie chaves API para acesso programático ao seu workspace.","settings.api_keys.create":"Criar chave API","settings.api_keys.copy":"Copiar","settings.api_keys.new_token_message":"Sua nova chave API foi criada. Copie agora — você não poderá vê-la novamente.","settings.api_keys.table.name":"Nome","settings.api_keys.table.key":"Chave","settings.api_keys.table.status":"Status","settings.api_keys.table.expires":"Expira","settings.api_keys.table.last_used":"Último uso","settings.api_keys.table.never":"Nunca","settings.api_keys.actions.copy_id":"Copiar ID da chave API","settings.api_keys.actions.copy_id_success":"ID da chave API copiado","settings.api_keys.actions.delete":"Excluir","settings.api_keys.empty.title":"Nenhuma chave API","settings.api_keys.empty.description":"Crie uma chave API para acessar seu workspace programaticamente.","settings.api_keys.delete_modal.title":"Excluir chave API","settings.api_keys.delete_modal.description":"Tem certeza que deseja excluir esta chave API? Aplicações que a usam perderão acesso imediatamente.","settings.api_keys.delete_modal.action":"Excluir chave API","settings.api_keys.create_dialog.title":"Criar chave API","settings.api_keys.create_dialog.description":"Crie uma nova chave API para acesso programático ao seu workspace.","settings.api_keys.create_dialog.name":"Nome","settings.api_keys.create_dialog.name_placeholder":"ex. Chave API de Produção","settings.api_keys.create_dialog.expires":"Data de expiração (opcional)","settings.api_keys.create_dialog.expires_placeholder":"Sem expiração","settings.api_keys.create_dialog.submit":"Criar","settings.api_keys.create_dialog.cancel":"Cancelar","settings.api_keys.flash.created":"Chave de API criada com sucesso!","settings.api_keys.flash.deleted":"Chave de API excluída com sucesso!","sidebar.workspaces":"Espaços de trabalho","sidebar.select_workspace":"Selecionar workspace","sidebar.create_workspace":"Criar workspace","sidebar.create_post":"Novo post","sidebar.profile":"Perfil","sidebar.log_out":"Sair","sidebar.workspace.connections":"Conexões","sidebar.workspace.hashtags":"Hashtags","sidebar.workspace.labels":"Etiquetas","sidebar.workspace.api_keys":"API Keys","sidebar.workspace.settings":"Configurações","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.workspace":"Workspace","sidebar.groups.account":"Conta","sidebar.groups.support":"Suporte","sidebar.analytics":"Analytics","sidebar.posts.calendar":"Calendário","sidebar.posts.all":"Todos","sidebar.posts.scheduled":"Agendados","sidebar.posts.posted":"Publicados","sidebar.posts.drafts":"Rascunhos","sidebar.account.settings":"Configurações","sidebar.account.usage":"Uso","sidebar.account.billing":"Faturamento","sidebar.notifications":"Notificações","sidebar.mark_all_read":"Marcar tudo como lido","sidebar.mark_as_read":"Marcar como lido","sidebar.archive_all":"Arquivar tudo","sidebar.no_notifications":"Sem notificações","sidebar.support.discord":"Discord","sidebar.support.share_feedback":"Enviar feedback","sidebar.support.last_updates":"Últimas Atualizações","sidebar.support.docs":"Documentação","usage.title":"Uso","usage.section_account":"Conta","usage.section_account_description":"Cotas e limites do seu plano :plan.","usage.section_ai":"Geração AI","usage.section_ai_description":"Uso de geração de imagens e vídeos AI do mês atual.","usage.section_data":"Dados","usage.section_data_description":"Retenção de dados e armazenamento do seu plano.","usage.workspaces":"Workspaces","usage.social_accounts":"Contas Sociais","usage.members":"Membros","usage.ai_images":"Imagens","usage.ai_videos":"Vídeos","usage.data_retention":"Retenção de Dados","usage.unlimited":"Ilimitado","usage.days":"dias","usage.year":"ano","usage.years":"anos","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","workspaces.title":"Workspaces","workspaces.select_title":"Seus workspaces","workspaces.select_description":"Selecione um workspace para continuar","workspaces.current":"Atual","workspaces.connections":":count conexões","workspaces.posts":":count posts","workspaces.create.title":"Criar um workspace","workspaces.create.description":"Dê um nome ao seu workspace para começar","workspaces.create.name":"Nome do workspace","workspaces.create.name_placeholder":"Meu Workspace","workspaces.create.submit":"Criar workspace"} \ No newline at end of file diff --git a/resources/css/app.css b/resources/css/app.css index ab96f12e..ea3cd0b0 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,6 +1,7 @@ @import 'tailwindcss'; @import 'tw-animate-css'; @import 'vue-sonner/style.css'; +@plugin '@tailwindcss/typography'; @source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; @source '../../storage/framework/views/*.php'; @custom-variant dark (&:is(.dark *)); diff --git a/resources/js/components/posts/editor/WritingAssistantTab.vue b/resources/js/components/posts/editor/WritingAssistantTab.vue index 3e904e3a..3c05b0fd 100644 --- a/resources/js/components/posts/editor/WritingAssistantTab.vue +++ b/resources/js/components/posts/editor/WritingAssistantTab.vue @@ -1,9 +1,10 @@