diff --git a/app/Actions/Ai/AutofillBrand.php b/app/Actions/Ai/AutofillBrand.php index f7128144..c8be82c5 100644 --- a/app/Actions/Ai/AutofillBrand.php +++ b/app/Actions/Ai/AutofillBrand.php @@ -4,383 +4,35 @@ namespace App\Actions\Ai; -use App\Ai\Agents\BrandAnalyzer; -use App\Models\Workspace; -use Illuminate\Http\Client\ConnectionException; -use Illuminate\Support\Facades\Http; -use Illuminate\Support\Facades\Log; -use Illuminate\Support\Str; -use League\HTMLToMarkdown\HtmlConverter; -use RuntimeException; -use Symfony\Component\DomCrawler\Crawler; -use Throwable; +use App\Services\Brand\BrandAnalyzerRunner; +use App\Services\Brand\BrandMetadata; +use App\Services\Brand\HomepageMetaExtractor; +use App\Services\Brand\SafeHttpFetcher; -class AutofillBrand +final class AutofillBrand { - private const REQUEST_TIMEOUT_SECONDS = 10; + public function __construct( + private readonly SafeHttpFetcher $fetcher, + private readonly HomepageMetaExtractor $extractor, + private readonly BrandAnalyzerRunner $analyzer, + ) {} - private const MAX_LOGO_BYTES = 2 * 1024 * 1024; - - private const ALLOWED_LOGO_MIME = ['image/png', 'image/jpeg', 'image/webp', 'image/gif', 'image/x-icon', 'image/vnd.microsoft.icon']; - - /** - * @return array{name: ?string, brand_description: ?string, content_language: ?string, brand_tone: ?string, brand_voice_notes: ?string, logo_url: ?string} - */ - public function __invoke(string $url, Workspace $workspace): array + public function __invoke(string $url): BrandMetadata { - $url = $this->normalizeUrl($url); - $this->guardAgainstSsrf($url); + $url = $this->fetcher->normalizeUrl($url); - $html = $this->fetchHtml($url); - $crawler = new Crawler($html, $url); + $html = $this->fetcher->get($url)->body(); - $logoUrl = $this->extractLogoUrl($crawler, $url); + $metadata = $this->extractor->extract($html, $url); - if ($logoUrl) { - $this->attachLogoToWorkspace($workspace, $logoUrl); + if (! $this->analyzer->isAvailable()) { + return $metadata; } - $result = [ - 'name' => $this->extractName($crawler), - 'brand_description' => $this->extractDescription($crawler), - 'content_language' => $this->extractLanguage($crawler), - 'brand_tone' => null, - 'brand_voice_notes' => null, - 'logo_url' => $logoUrl, - ]; + $analysis = $this->analyzer->analyze($this->extractor->extractBodyHtml($html)); - if ($this->isLlmAvailable()) { - $llm = $this->analyzeWithLlm($crawler); - - if ($llm !== null) { - $result['brand_description'] = $llm['description'] ?? $result['brand_description']; - $result['content_language'] = $llm['language'] ?? $result['content_language']; - $result['brand_tone'] = $llm['tone'] ?? null; - $result['brand_voice_notes'] = $llm['voice_notes'] ?? null; - } - } - - return $result; - } - - private function isLlmAvailable(): bool - { - $provider = config('trypost.ai.text_provider'); - - return match ($provider) { - 'openai' => ! empty(config('services.openai.api_key')), - 'gemini' => ! empty(config('services.gemini.api_key')), - default => false, - }; - } - - /** - * @return array{description: string, tone: string, language: string, voice_notes: string}|null - */ - private function analyzeWithLlm(Crawler $crawler): ?array - { - $markdown = $this->buildMarkdown($crawler); - - if ($markdown === '') { - return null; - } - - try { - $response = (new BrandAnalyzer)->prompt($markdown); - } catch (Throwable $e) { - Log::warning('BrandAnalyzer failed, falling back to meta tags', ['error' => $e->getMessage()]); - - return null; - } - - return [ - 'description' => (string) ($response['description'] ?? ''), - 'tone' => (string) ($response['tone'] ?? ''), - 'language' => (string) ($response['language'] ?? ''), - 'voice_notes' => (string) ($response['voice_notes'] ?? ''), - ]; - } - - private function buildMarkdown(Crawler $crawler): string - { - $body = $crawler->filter('main')->first(); - - if ($body->count() === 0) { - $body = $crawler->filter('body')->first(); - } - - if ($body->count() === 0) { - return ''; - } - - // Strip noise before converting. - foreach (['script', 'style', 'nav', 'footer', 'noscript'] as $selector) { - $body->filter($selector)->each(function (Crawler $node) { - $domNode = $node->getNode(0); - $domNode?->parentNode?->removeChild($domNode); - }); - } - - $html = $body->html(); - $markdown = (new HtmlConverter(['strip_tags' => true]))->convert($html); - - return Str::limit(trim($markdown), 4000, ''); - } - - private function normalizeUrl(string $url): string - { - $url = trim($url); - - if (preg_match('~^[a-z][a-z0-9+.-]*://~i', $url)) { - return $url; - } - - return 'https://'.$url; - } - - private function guardAgainstSsrf(string $url): void - { - $parts = parse_url($url); - - if (! $parts || ! in_array(strtolower(data_get($parts, 'scheme', '')), ['http', 'https'], true)) { - throw new RuntimeException('Only http:// and https:// URLs are supported.'); - } - - $host = data_get($parts, 'host'); - - if (! $host) { - throw new RuntimeException('URL is missing a host.'); - } - - $ip = gethostbyname($host); - - if ($ip === $host && ! filter_var($host, FILTER_VALIDATE_IP)) { - throw new RuntimeException("Could not resolve host: {$host}"); - } - - $isPublic = filter_var( - $ip, - FILTER_VALIDATE_IP, - FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE, - ); - - if (! $isPublic) { - throw new RuntimeException('Internal or private network addresses are not allowed.'); - } - } - - private function fetchHtml(string $url): string - { - try { - $response = Http::timeout(self::REQUEST_TIMEOUT_SECONDS) - ->withUserAgent('TryPostBot/1.0 (+https://trypost.it)') - ->withOptions(['allow_redirects' => ['max' => 3]]) - ->get($url); - } catch (ConnectionException $e) { - throw new RuntimeException("Could not reach the website: {$e->getMessage()}"); - } - - if ($response->failed()) { - throw new RuntimeException("Website returned HTTP {$response->status()}."); - } - - return $response->body(); - } - - private function extractName(Crawler $crawler): ?string - { - $ogSiteName = $this->metaContent($crawler, 'property', 'og:site_name'); - if ($ogSiteName) { - return $ogSiteName; - } - - $ogTitle = $this->metaContent($crawler, 'property', 'og:title'); - if ($ogTitle) { - return $this->stripTitleSuffix($ogTitle); - } - - $title = $crawler->filter('title')->first(); - if ($title->count() > 0) { - return $this->stripTitleSuffix(trim($title->text(''))) ?: null; - } - - return null; - } - - private function extractDescription(Crawler $crawler): ?string - { - return $this->metaContent($crawler, 'name', 'description') - ?? $this->metaContent($crawler, 'property', 'og:description'); - } - - private function extractLanguage(Crawler $crawler): ?string - { - $html = $crawler->filter('html')->first(); - - if ($html->count() === 0) { - return null; - } - - $lang = trim((string) $html->attr('lang', '')); - - if ($lang === '') { - return null; - } - - return $this->normalizeLanguageCode($lang); - } - - private function normalizeLanguageCode(string $raw): ?string - { - $lower = strtolower($raw); - - return match (true) { - str_starts_with($lower, 'pt') => 'pt-BR', - str_starts_with($lower, 'es') => 'es', - str_starts_with($lower, 'en') => 'en', - default => null, - }; - } - - private function extractLogoUrl(Crawler $crawler, string $baseUrl): ?string - { - $candidates = []; - - foreach ($crawler->filter('link[rel="apple-touch-icon"]') as $node) { - $href = $node->getAttribute('href'); - if ($href) { - $candidates[] = ['href' => $href, 'priority' => 100, 'size' => $this->parseIconSize($node->getAttribute('sizes'))]; - } - } - - foreach ($crawler->filter('link[rel*="icon"]') as $node) { - $href = $node->getAttribute('href'); - if ($href) { - $candidates[] = ['href' => $href, 'priority' => 50, 'size' => $this->parseIconSize($node->getAttribute('sizes'))]; - } - } - - $ogImage = $this->metaContent($crawler, 'property', 'og:image'); - if ($ogImage) { - $candidates[] = ['href' => $ogImage, 'priority' => 25, 'size' => 0]; - } - - if (empty($candidates)) { - return null; - } - - usort($candidates, fn ($a, $b) => $b['priority'] <=> $a['priority'] ?: $b['size'] <=> $a['size']); - - return $this->resolveUrl($candidates[0]['href'], $baseUrl); - } - - private function parseIconSize(?string $sizes): int - { - if (! $sizes) { - return 0; - } - - preg_match_all('/(\d+)x\d+/', $sizes, $matches); - - return $matches[1] === [] ? 0 : max(array_map('intval', $matches[1])); - } - - private function resolveUrl(string $href, string $baseUrl): string - { - if (preg_match('~^https?://~i', $href)) { - return $href; - } - - $base = parse_url($baseUrl); - $scheme = data_get($base, 'scheme', 'https'); - $host = data_get($base, 'host'); - - if (str_starts_with($href, '//')) { - return "{$scheme}:{$href}"; - } - - if (str_starts_with($href, '/')) { - return "{$scheme}://{$host}{$href}"; - } - - return "{$scheme}://{$host}/{$href}"; - } - - private function attachLogoToWorkspace(Workspace $workspace, string $logoUrl): void - { - $this->guardAgainstSsrf($logoUrl); - - try { - $response = Http::timeout(self::REQUEST_TIMEOUT_SECONDS) - ->withUserAgent('TryPostBot/1.0 (+https://trypost.it)') - ->get($logoUrl); - } catch (ConnectionException) { - return; - } - - if ($response->failed()) { - return; - } - - $contentType = strtolower(explode(';', (string) $response->header('Content-Type'))[0]); - - if (! in_array($contentType, self::ALLOWED_LOGO_MIME, true)) { - return; - } - - $body = $response->body(); - - if (strlen($body) > self::MAX_LOGO_BYTES) { - return; - } - - $extension = $this->extensionForMime($contentType); - $tempPath = tempnam(sys_get_temp_dir(), 'logo_').'.'.$extension; - file_put_contents($tempPath, $body); - - try { - $workspace->clearMediaCollection('logo'); - $workspace->addMediaFromPath($tempPath, 'logo.'.$extension, 'logo', ['ai_autofill' => true]); - } finally { - if (file_exists($tempPath)) { - @unlink($tempPath); - } - } - } - - private function extensionForMime(string $mime): string - { - return match ($mime) { - 'image/jpeg' => 'jpg', - 'image/webp' => 'webp', - 'image/gif' => 'gif', - 'image/x-icon', 'image/vnd.microsoft.icon' => 'ico', - default => 'png', - }; - } - - private function metaContent(Crawler $crawler, string $attr, string $value): ?string - { - $node = $crawler->filter("meta[{$attr}=\"{$value}\"]")->first(); - - if ($node->count() === 0) { - return null; - } - - $content = trim((string) $node->attr('content', '')); - - return $content === '' ? null : $content; - } - - private function stripTitleSuffix(string $title): string - { - foreach ([' | ', ' - ', ' — ', ' – '] as $sep) { - $idx = mb_strpos($title, $sep); - if ($idx !== false && $idx > 0) { - return trim(mb_substr($title, 0, $idx)); - } - } - - return trim($title); + return $analysis === null + ? $metadata + : $metadata->mergeLlm($analysis); } } diff --git a/app/Actions/Post/CreatePost.php b/app/Actions/Post/CreatePost.php index 9c2c1c6a..413bdfbe 100644 --- a/app/Actions/Post/CreatePost.php +++ b/app/Actions/Post/CreatePost.php @@ -5,8 +5,6 @@ namespace App\Actions\Post; use App\Enums\Post\Status as PostStatus; -use App\Enums\PostPlatform\ContentType; -use App\Enums\PostPlatform\Status as PostPlatformStatus; use App\Models\Post; use App\Models\User; use App\Models\Workspace; @@ -16,8 +14,8 @@ class CreatePost { public static function execute(Workspace $workspace, User $user, array $data): Post { - $date = data_get($data, 'date') ?: Carbon::now($workspace->timezone)->format('Y-m-d'); - $scheduledAt = Carbon::parse($date, $workspace->timezone) + $date = data_get($data, 'date') ?: Carbon::now('UTC')->format('Y-m-d'); + $scheduledAt = Carbon::parse($date, 'UTC') ->setTime(9, 0) ->utc(); @@ -29,20 +27,7 @@ public static function execute(Workspace $workspace, User $user, array $data): P 'scheduled_at' => $scheduledAt, ]); - $socialAccounts = $workspace->socialAccounts()->active()->get(); - - foreach ($socialAccounts as $account) { - $post->postPlatforms()->create([ - 'social_account_id' => $account->id, - 'platform' => $account->platform->value, - 'platform_name' => $account->display_name, - 'platform_username' => $account->username, - 'platform_avatar' => $account->getRawOriginal('avatar_url'), - 'content_type' => ContentType::defaultFor($account->platform), - 'status' => PostPlatformStatus::Pending, - 'enabled' => false, - ]); - } + SyncPostPlatforms::execute($post); return $post; } diff --git a/app/Actions/Post/SyncPostPlatforms.php b/app/Actions/Post/SyncPostPlatforms.php new file mode 100644 index 00000000..20fa7780 --- /dev/null +++ b/app/Actions/Post/SyncPostPlatforms.php @@ -0,0 +1,43 @@ +workspace; + + $existingAccountIds = $post->postPlatforms()->pluck('social_account_id')->filter(); + + $missingAccounts = $workspace->socialAccounts() + ->active() + ->whereNotIn('id', $existingAccountIds) + ->get(); + + foreach ($missingAccounts as $account) { + $post->postPlatforms()->create([ + 'social_account_id' => $account->id, + 'platform' => $account->platform->value, + 'platform_name' => $account->display_name, + 'platform_username' => $account->username, + 'platform_avatar' => $account->getRawOriginal('avatar_url'), + 'content_type' => ContentType::defaultFor($account->platform), + 'status' => PostPlatformStatus::Pending, + 'enabled' => false, + ]); + } + } +} diff --git a/app/Actions/Post/UpdatePost.php b/app/Actions/Post/UpdatePost.php index 3648f5db..ef40bd96 100644 --- a/app/Actions/Post/UpdatePost.php +++ b/app/Actions/Post/UpdatePost.php @@ -26,7 +26,7 @@ public static function execute(Workspace $workspace, Post $post, array $data): a $scheduledAt = $post->scheduled_at; if (data_get($data, 'scheduled_at')) { - $scheduledAt = Carbon::parse(data_get($data, 'scheduled_at'), $workspace->timezone)->utc(); + $scheduledAt = Carbon::parse(data_get($data, 'scheduled_at'))->utc(); } $status = data_get($data, 'status', $post->status); diff --git a/app/Actions/User/CreateUser.php b/app/Actions/User/CreateUser.php index f1169a24..bd06de98 100644 --- a/app/Actions/User/CreateUser.php +++ b/app/Actions/User/CreateUser.php @@ -4,17 +4,14 @@ namespace App\Actions\User; -use App\Enums\User\Setup; -use App\Enums\UserWorkspace\Role; use App\Models\Account; use App\Models\User; -use App\Models\Workspace; use Illuminate\Support\Facades\DB; class CreateUser { /** - * @param array{name: string, email: string, password?: string, timezone?: string, setup?: Setup, email_verified_at?: \DateTimeInterface|null} $data + * @param array{name: string, email: string, password?: string, email_verified_at?: \DateTimeInterface|null, is_invite?: bool} $data */ public static function execute(array $data): User { @@ -30,24 +27,12 @@ public static function execute(array $data): User 'name' => data_get($data, 'name'), 'email' => data_get($data, 'email'), 'password' => data_get($data, 'password'), - 'setup' => data_get($data, 'setup', $isInviteRegistration ? Setup::Completed : Setup::Role), 'email_verified_at' => data_get($data, 'email_verified_at', $isInviteRegistration ? now() : null), 'account_id' => $account->id, ]); $account->update(['owner_id' => $user->id]); - $workspace = Workspace::create([ - 'account_id' => $account->id, - 'user_id' => $user->id, - 'name' => $user->name."'s Workspace", - 'timezone' => data_get($data, 'timezone', 'UTC'), - ]); - - $workspace->members()->attach($user->id, ['role' => Role::Member->value]); - - $user->update(['current_workspace_id' => $workspace->id]); - return $user; }); } diff --git a/app/Actions/Workspace/CreateWorkspace.php b/app/Actions/Workspace/CreateWorkspace.php index 3b727868..deb83a99 100644 --- a/app/Actions/Workspace/CreateWorkspace.php +++ b/app/Actions/Workspace/CreateWorkspace.php @@ -10,13 +10,24 @@ class CreateWorkspace { + /** + * @param array $data + */ public static function execute(User $user, array $data): Workspace { + $attributes = array_filter([ + 'name' => data_get($data, 'name'), + 'brand_website' => data_get($data, 'brand_website'), + 'brand_description' => data_get($data, 'brand_description'), + 'brand_tone' => data_get($data, 'brand_tone'), + 'brand_voice_notes' => data_get($data, 'brand_voice_notes'), + 'content_language' => data_get($data, 'content_language'), + ], static fn ($value): bool => $value !== null); + $workspace = Workspace::create([ + ...$attributes, 'account_id' => $user->account_id, 'user_id' => $user->id, - ...$data, - 'timezone' => config('app.timezone', 'UTC'), ]); $workspace->members()->attach($user->id, ['role' => Role::Member->value]); diff --git a/app/Ai/Agents/BrandAnalyzer.php b/app/Ai/Agents/BrandAnalyzer.php index 4f75ab9b..612d09b9 100644 --- a/app/Ai/Agents/BrandAnalyzer.php +++ b/app/Ai/Agents/BrandAnalyzer.php @@ -30,6 +30,9 @@ public function provider(): Lab public function schema(JsonSchema $schema): array { return [ + 'name' => $schema->string() + ->description('The actual brand or company name (1-4 words, e.g. "Sendkit", "Acme Coffee", "Stripe"). Strip any tagline, slogan, or product descriptor — return only the brand identity itself.') + ->required(), 'description' => $schema->string() ->description('A concise 2-3 sentence brand description summarizing what the company does, who they serve, and what makes them unique. Written in the detected content language.') ->required(), diff --git a/app/Ai/Agents/Humanizer.php b/app/Ai/Agents/Humanizer.php new file mode 100644 index 00000000..97da6061 --- /dev/null +++ b/app/Ai/Agents/Humanizer.php @@ -0,0 +1,31 @@ +instructions; + } + + public function provider(): Lab + { + return match (config('trypost.ai.text_provider')) { + 'openai' => Lab::OpenAI, + default => Lab::Gemini, + }; + } +} diff --git a/app/Ai/Agents/SocialMediaAssistant.php b/app/Ai/Agents/SocialMediaAssistant.php index 09765c74..9c29b705 100644 --- a/app/Ai/Agents/SocialMediaAssistant.php +++ b/app/Ai/Agents/SocialMediaAssistant.php @@ -4,25 +4,32 @@ namespace App\Ai\Agents; +use App\Ai\Middleware\DebugGeminiRequest; use App\Ai\PlatformRules\Contract; use App\Ai\PlatformRules\Registry as PlatformRulesRegistry; -use App\Ai\Tools\GenerateAudio; use App\Ai\Tools\GenerateImage; use App\Ai\Tools\GenerateVideo; use App\Enums\SocialAccount\Platform; use App\Models\AiMessage; use App\Models\Post; use App\Models\Workspace; +use Illuminate\Contracts\JsonSchema\JsonSchema; use Illuminate\Support\Str; +use Laravel\Ai\Attributes\MaxSteps; +use Laravel\Ai\Attributes\Temperature; use Laravel\Ai\Contracts\Agent; use Laravel\Ai\Contracts\Conversational; +use Laravel\Ai\Contracts\HasMiddleware; +use Laravel\Ai\Contracts\HasStructuredOutput; use Laravel\Ai\Contracts\HasTools; use Laravel\Ai\Contracts\Tool; use Laravel\Ai\Enums\Lab; use Laravel\Ai\Messages\Message; use Laravel\Ai\Promptable; -class SocialMediaAssistant implements Agent, Conversational, HasTools +#[Temperature(0.3)] +#[MaxSteps(1)] +class SocialMediaAssistant implements Agent, Conversational, HasMiddleware, HasStructuredOutput, HasTools { use Promptable; @@ -42,6 +49,7 @@ public function instructions(): string 'voice_notes' => $this->workspace->brand_voice_notes ?? '', 'content_language' => $this->workspace->content_language ?? 'en', 'platform_rules' => $this->activePlatformRules(), + 'connected_platforms' => $this->connectedPlatformLabels(), ])->render(); } @@ -71,6 +79,23 @@ private function activePlatformRules(): array return PlatformRulesRegistry::forMany($platforms); } + /** + * @return array + */ + private function connectedPlatformLabels(): array + { + return $this->workspace->socialAccounts() + ->active() + ->get() + ->map(fn ($account) => [ + 'slug' => $account->platform->value, + 'label' => $account->platform->label(), + ]) + ->unique('slug') + ->values() + ->all(); + } + /** * @return iterable */ @@ -84,6 +109,7 @@ public function messages(): iterable ->where('post_id', $this->post->id) ->whereIn('role', ['user', 'assistant']) ->oldest() + ->orderBy('id') ->limit(20) ->get() ->map(fn (AiMessage $m) => new Message($m->role, $this->enrichContent($m))) @@ -98,6 +124,29 @@ public function provider(): Lab }; } + public function middleware(): array + { + return [ + new DebugGeminiRequest, + ]; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'message' => $schema->string() + ->description('Your response. 1-2 sentences max. No emojis. No brand pitching.') + ->required(), + 'quick_actions' => $schema->array() + ->items($schema->object(fn ($s) => [ + 'label' => $s->string()->description('Button text, no emojis, max 20 chars.')->required(), + 'value' => $s->string()->description('Same as label.')->required(), + ])) + ->description('Buttons for FINITE choices only (format, platform, confirm). Empty array for open-ended questions. Max 4 items.') + ->required(), + ]; + } + /** * @return iterable */ @@ -114,11 +163,6 @@ public function tools(): iterable post: $this->post, userId: $this->userId, ), - new GenerateAudio( - workspace: $this->workspace, - post: $this->post, - userId: $this->userId, - ), ]; } diff --git a/app/Ai/Middleware/DebugGeminiRequest.php b/app/Ai/Middleware/DebugGeminiRequest.php new file mode 100644 index 00000000..d8d0212b --- /dev/null +++ b/app/Ai/Middleware/DebugGeminiRequest.php @@ -0,0 +1,37 @@ + get_class($prompt->agent), + 'model' => $prompt->model, + 'has_structured_output' => $prompt->agent instanceof HasStructuredOutput, + 'has_tools' => $prompt->agent instanceof HasTools, + 'tools_count' => $prompt->agent instanceof HasTools ? count(iterator_to_array($prompt->agent->tools())) : 0, + ]); + + return $next($prompt)->then(function (AgentResponse $response) { + Log::debug('Agent response debug', [ + 'response_class' => get_class($response), + 'text_length' => strlen($response->text ?? ''), + 'text_preview' => substr($response->text ?? '', 0, 200), + 'is_structured' => $response instanceof StructuredAgentResponse, + 'structured_data' => $response instanceof StructuredAgentResponse ? $response->structured : null, + ]); + }); + } +} diff --git a/app/Ai/PlatformRules/InstagramRules.php b/app/Ai/PlatformRules/InstagramRules.php index 61412d91..dc844352 100644 --- a/app/Ai/PlatformRules/InstagramRules.php +++ b/app/Ai/PlatformRules/InstagramRules.php @@ -32,7 +32,7 @@ public function specs(): array public function summary(): string { return <<<'TXT' -Instagram: caption max 2200 chars, up to 30 hashtags (3-5 recommended). +Instagram: caption max 2200 chars. - Feed: square (1:1) or portrait (4:5). Up to 10 images/videos. - Carousel: 2-10 slides, all same aspect ratio. First slide is the hook. - Reel: vertical 9:16 video only, up to 90s (30-60s performs best). Hook in first 1-3s. diff --git a/app/Ai/Tools/GenerateImage.php b/app/Ai/Tools/GenerateImage.php index e3e738f2..3faa77e9 100644 --- a/app/Ai/Tools/GenerateImage.php +++ b/app/Ai/Tools/GenerateImage.php @@ -37,12 +37,15 @@ public function description(): Stringable|string Call this when the user asks for an image, photo, carousel slide, or any visual content. Pass a detailed visual prompt describing what to generate, and an orientation: -- "vertical" (9:16) for Instagram Reel/Story, Pinterest, TikTok, YouTube Shorts -- "horizontal" (16:9) for X/Twitter, LinkedIn, Facebook +- "square" (1:1) for LinkedIn, Facebook, or when user wants square +- "portrait" (4:5) for Instagram Feed, Threads +- "vertical" (9:16) for Instagram Reel/Story, Pinterest Pin, TikTok +- "horizontal" (16:9) for X/Twitter, YouTube thumbnail -The image is generated, stored on the public disk, registered in the workspace's -media library, logged in monthly usage tracking, and attached to the assistant's -response message. +Choose the orientation that best matches the target platform. + +The image is generated, stored, registered in the workspace's media library, +logged in monthly usage tracking, and attached to the assistant's response message. TXT; } @@ -57,7 +60,7 @@ public function handle(Request $request): Stringable|string $prompt = (string) data_get($request, 'prompt', ''); $orientationString = (string) data_get($request, 'orientation', 'vertical'); - $orientationEnum = Orientation::tryFrom($orientationString) ?? Orientation::Vertical; + $orientationEnum = Orientation::tryFrom($orientationString) ?? Orientation::Portrait; $aspectRatio = $orientationEnum->aspectRatio(); $renderedPrompt = view('prompts.assistant.image', [ @@ -73,7 +76,7 @@ public function handle(Request $request): Stringable|string ->quality('high') ->generate(); - $storedPath = $response->store('medias', 'public'); + $storedPath = $response->store('medias'); $media = $this->workspace->media()->create([ 'group_id' => Str::uuid()->toString(), @@ -82,7 +85,7 @@ public function handle(Request $request): Stringable|string 'path' => $storedPath, 'original_filename' => 'ai-generated.png', 'mime_type' => 'image/png', - 'size' => Storage::disk('public')->size($storedPath), + 'size' => Storage::size($storedPath), 'order' => 0, 'meta' => ['ai_generated' => true, 'prompt' => Str::limit($prompt, 200)], ]); @@ -114,8 +117,8 @@ public function schema(JsonSchema $schema): array ->description('A detailed visual description of the image to generate. Include subject, style, composition, mood, and any text that should appear in the image.') ->required(), 'orientation' => $schema->string() - ->enum(['vertical', 'horizontal']) - ->description('"vertical" for 9:16 (Instagram Reel/Story, TikTok, YouTube Shorts, Pinterest). "horizontal" for 16:9 (X/Twitter, LinkedIn, Facebook).') + ->enum(['square', 'portrait', 'vertical', 'horizontal']) + ->description('"square" (1:1) for LinkedIn, Facebook. "portrait" (4:5) for Instagram Feed, Threads. "vertical" (9:16) for Instagram Reel/Story, Pinterest, TikTok. "horizontal" (16:9) for X/Twitter.') ->required(), ]; } diff --git a/app/Enums/Ai/Orientation.php b/app/Enums/Ai/Orientation.php index 66d7308f..2907578d 100644 --- a/app/Enums/Ai/Orientation.php +++ b/app/Enums/Ai/Orientation.php @@ -6,12 +6,16 @@ enum Orientation: string { + case Square = 'square'; + case Portrait = 'portrait'; case Vertical = 'vertical'; case Horizontal = 'horizontal'; public function aspectRatio(): string { return match ($this) { + self::Square => '1:1', + self::Portrait => '4:5', self::Vertical => '9:16', self::Horizontal => '16:9', }; diff --git a/app/Enums/AiMessage/Status.php b/app/Enums/AiMessage/Status.php new file mode 100644 index 00000000..06e1484b --- /dev/null +++ b/app/Enums/AiMessage/Status.php @@ -0,0 +1,13 @@ +value}.label"); - } - - public function description(): string - { - return __("onboarding.personas.{$this->value}.description"); - } - - public function icon(): string - { - return match ($this) { - self::Founder => 'rocket', - self::Creator => 'sparkles', - self::Agency => 'building', - self::Enterprise => 'building-2', - self::SmallBusiness => 'store', - self::Personal => 'user', - }; - } - - /** - * @return array - */ - public static function toSelectArray(): array - { - return array_map( - fn (self $case) => [ - 'value' => $case->value, - 'label' => $case->label(), - 'description' => $case->description(), - 'icon' => $case->icon(), - ], - self::cases() - ); - } -} diff --git a/app/Enums/User/Setup.php b/app/Enums/User/Setup.php deleted file mode 100644 index c0b1451b..00000000 --- a/app/Enums/User/Setup.php +++ /dev/null @@ -1,39 +0,0 @@ - 'Registering', - self::Role => 'Select Role', - self::Brand => 'Configure Brand', - self::Connections => 'Connect Accounts', - self::Subscription => 'Start Subscription', - self::Completed => 'Completed', - }; - } - - public function stepNumber(): int - { - return match ($this) { - self::Registering => 0, - self::Role => 1, - self::Brand => 2, - self::Connections => 3, - self::Subscription => 4, - self::Completed => 5, - }; - } -} diff --git a/app/Events/Ai/AssistantMessageUpdated.php b/app/Events/Ai/AssistantMessageUpdated.php new file mode 100644 index 00000000..3f760816 --- /dev/null +++ b/app/Events/Ai/AssistantMessageUpdated.php @@ -0,0 +1,52 @@ +message->post_id), + ]; + } + + public function broadcastWith(): array + { + $this->message->refresh(); + + return [ + 'message' => [ + 'id' => $this->message->id, + 'post_id' => $this->message->post_id, + 'role' => $this->message->role, + 'content' => $this->message->content, + 'content_html' => $this->message->content_html, + 'attachments' => $this->message->attachments, + 'status' => $this->message->status->value, + 'error_message' => $this->message->error_message, + 'metadata' => $this->message->metadata, + 'created_at' => $this->message->created_at->toISOString(), + 'updated_at' => $this->message->updated_at->toISOString(), + ], + ]; + } +} diff --git a/app/Http/Controllers/App/AssetController.php b/app/Http/Controllers/App/AssetController.php index 06a6a76d..f1e32288 100644 --- a/app/Http/Controllers/App/AssetController.php +++ b/app/Http/Controllers/App/AssetController.php @@ -6,6 +6,7 @@ use App\Http\Requests\App\Asset\StoreAssetFromUrlRequest; use App\Http\Requests\App\Asset\StoreAssetRequest; +use App\Http\Resources\App\MediaResource; use App\Models\Media; use App\Services\UnsplashService; use Illuminate\Http\JsonResponse; @@ -39,7 +40,7 @@ public function index(Request $request): Response|RedirectResponse ]); } - public function store(StoreAssetRequest $request): JsonResponse + public function store(StoreAssetRequest $request): MediaResource { $workspace = $request->user()->currentWorkspace; @@ -47,17 +48,7 @@ public function store(StoreAssetRequest $request): JsonResponse $media = $workspace->addMedia($request->file('media'), 'assets'); - return response()->json([ - 'id' => $media->id, - 'path' => $media->path, - 'url' => $media->url, - 'type' => $media->type->value, - 'mime_type' => $media->mime_type, - 'original_filename' => $media->original_filename, - 'size' => $media->size, - 'meta' => $media->meta, - 'created_at' => $media->created_at->toISOString(), - ]); + return new MediaResource($media); } public function storeChunked(Request $request): JsonResponse @@ -124,7 +115,7 @@ public function storeChunked(Request $request): JsonResponse ]); } - public function storeFromUrl(StoreAssetFromUrlRequest $request, UnsplashService $unsplash): RedirectResponse + public function storeFromUrl(StoreAssetFromUrlRequest $request, UnsplashService $unsplash): MediaResource { $workspace = $request->user()->currentWorkspace; @@ -166,7 +157,7 @@ public function storeFromUrl(StoreAssetFromUrlRequest $request, UnsplashService } @unlink($tempFile); - $workspace->media()->create([ + $media = $workspace->media()->create([ 'group_id' => Str::uuid()->toString(), 'collection' => 'assets', 'type' => 'image', @@ -178,10 +169,7 @@ public function storeFromUrl(StoreAssetFromUrlRequest $request, UnsplashService 'meta' => $meta, ]); - session()->flash('flash.banner', __('assets.saved')); - session()->flash('flash.bannerStyle', 'success'); - - return back(); + return new MediaResource($media); } public function destroy(Request $request, Media $media): RedirectResponse diff --git a/app/Http/Controllers/App/OnboardingController.php b/app/Http/Controllers/App/OnboardingController.php deleted file mode 100644 index 3eba1664..00000000 --- a/app/Http/Controllers/App/OnboardingController.php +++ /dev/null @@ -1,179 +0,0 @@ -enforceStep($request->user(), Setup::Role); - if ($redirect) { - return $redirect; - } - - return Inertia::render('onboarding/Role', [ - 'personas' => Persona::toSelectArray(), - ]); - } - - public function storeRole(Request $request): RedirectResponse - { - $validated = $request->validate([ - 'persona' => ['required', Rule::enum(Persona::class)], - ]); - - $request->user()->update([ - 'persona' => data_get($validated, 'persona'), - 'setup' => Setup::Brand, - ]); - - return redirect()->route('app.onboarding.brand'); - } - - public function brand(Request $request): Response|RedirectResponse - { - $redirect = $this->enforceStep($request->user(), Setup::Brand); - if ($redirect) { - return $redirect; - } - - $workspace = $request->user()->currentWorkspace; - - return Inertia::render('onboarding/Brand', [ - 'workspace' => [ - 'name' => $workspace?->name ?? '', - 'brand_website' => $workspace?->brand_website ?? '', - 'brand_description' => $workspace?->brand_description ?? '', - 'brand_tone' => $workspace?->brand_tone ?? 'professional', - 'brand_voice_notes' => $workspace?->brand_voice_notes ?? '', - 'content_language' => $workspace?->content_language ?? 'en', - ], - ]); - } - - public function storeBrand(StoreBrandRequest $request): RedirectResponse - { - $workspace = $request->user()->currentWorkspace; - - if ($workspace) { - $workspace->update($request->validated()); - } - - $request->user()->update(['setup' => Setup::Connections]); - - return redirect()->route('app.onboarding.account'); - } - - public function skipBrand(Request $request): RedirectResponse - { - $request->user()->update(['setup' => Setup::Connections]); - - return redirect()->route('app.onboarding.account'); - } - - public function autofillBrand(Request $request, AutofillBrand $autofill): JsonResponse - { - $validated = $request->validate([ - 'url' => ['required', 'string', 'max:255'], - ]); - - $workspace = $request->user()->currentWorkspace; - - if (! $workspace) { - abort(SymfonyResponse::HTTP_BAD_REQUEST, 'No workspace found.'); - } - - try { - $result = $autofill(data_get($validated, 'url'), $workspace); - } catch (RuntimeException $e) { - return response()->json(['message' => $e->getMessage()], SymfonyResponse::HTTP_UNPROCESSABLE_ENTITY); - } - - $workspace->refresh(); - - return response()->json([ - ...$result, - 'logo_url' => $workspace->logo_url, - 'has_logo' => $workspace->has_logo, - ]); - } - - public function account(Request $request): Response|RedirectResponse - { - $redirect = $this->enforceStep($request->user(), Setup::Connections); - if ($redirect) { - return $redirect; - } - - $user = $request->user(); - $workspace = $user->currentWorkspace; - - $platforms = collect(); - - if ($workspace) { - $connectedAccounts = $workspace->socialAccounts; - - $platforms = collect(SocialPlatform::enabled())->map(fn ($platform) => [ - 'value' => $platform->value, - 'label' => $platform->label(), - 'color' => $platform->color(), - 'connected' => $connectedAccounts->firstWhere('platform', $platform) !== null, - 'account' => $connectedAccounts->firstWhere('platform', $platform), - ])->values(); - } - - return Inertia::render('onboarding/Account', [ - 'platforms' => $platforms, - 'hasWorkspace' => $workspace !== null, - ]); - } - - public function storeAccount(Request $request): RedirectResponse - { - $request->user()->update(['setup' => Setup::Completed]); - - if (config('trypost.self_hosted')) { - session()->flash('flash.banner', __('auth.flash.welcome')); - session()->flash('flash.bannerStyle', 'success'); - - return redirect()->route('app.calendar'); - } - - return redirect()->route('app.subscribe'); - } - - private function enforceStep(User $user, Setup $expectedStep): ?RedirectResponse - { - if ($user->setup === $expectedStep) { - return null; - } - - if ($user->setup === Setup::Completed) { - return redirect()->route('app.calendar'); - } - - return match ($user->setup) { - Setup::Role => redirect()->route('app.onboarding.role'), - Setup::Brand => redirect()->route('app.onboarding.brand'), - Setup::Connections => redirect()->route('app.onboarding.account'), - default => redirect()->route('app.onboarding.role'), - }; - } -} diff --git a/app/Http/Controllers/App/PostAssistantController.php b/app/Http/Controllers/App/PostAssistantController.php index 79540f9f..f09a1e05 100644 --- a/app/Http/Controllers/App/PostAssistantController.php +++ b/app/Http/Controllers/App/PostAssistantController.php @@ -4,23 +4,15 @@ namespace App\Http\Controllers\App; -use App\Ai\Agents\SocialMediaAssistant; -use App\Ai\Tools\AttachmentCollector; use App\Enums\Ai\Intent; -use App\Enums\Ai\UsageType; -use App\Features\AiImagesLimit; -use App\Features\AiVideosLimit; +use App\Enums\AiMessage\Status; use App\Http\Requests\App\Assistant\StoreAssistantMessageRequest; -use App\Models\AiUsageLog; +use App\Jobs\Ai\GenerateAssistantResponse; use App\Models\Post; use App\Services\Ai\IntentDetector; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Log; -use Laravel\Pennant\Feature; -use RuntimeException; use Symfony\Component\HttpFoundation\Response; -use Throwable; class PostAssistantController extends Controller { @@ -35,6 +27,7 @@ public function index(Request $request, Post $post): JsonResponse $messages = $post->aiMessages() ->with('user') ->oldest() + ->orderBy('id') ->get(); return response()->json(['messages' => $messages]); @@ -44,7 +37,6 @@ public function store( StoreAssistantMessageRequest $request, Post $post, IntentDetector $intentDetector, - AttachmentCollector $collector, ): JsonResponse { $workspace = $request->user()->currentWorkspace; @@ -60,6 +52,7 @@ public function store( 'user_id' => $request->user()->id, 'role' => 'user', 'content' => $prompt, + 'status' => Status::Completed, ]); if ($request->hasFile('image')) { @@ -78,6 +71,7 @@ public function store( $assistantMessage = $post->aiMessages()->create([ 'role' => 'assistant', 'content' => __('assistant.content_blocked'), + 'status' => Status::Completed, 'metadata' => ['intent' => $intent->value, 'error' => true], ]); @@ -87,87 +81,22 @@ public function store( ], Response::HTTP_CREATED); } - try { - $post->loadMissing('postPlatforms'); + $assistantMessage = $post->aiMessages()->create([ + 'role' => 'assistant', + 'content' => '', + 'status' => Status::Pending, + 'metadata' => ['intent' => $intent->value], + ]); - $assistantMessages = $post->aiMessages() - ->where('role', 'assistant') - ->get(); + GenerateAssistantResponse::dispatch( + assistantMessage: $assistantMessage, + prompt: $prompt, + intent: $intent->value, + ); - $imagesInThread = $assistantMessages->sum( - fn ($m) => collect($m->attachments ?? [])->where('type', 'image')->count() - ); - - $videosInThread = $assistantMessages->sum( - fn ($m) => collect($m->attachments ?? [])->where('type', 'video')->count() - ); - - $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}"; - - $collector->clear(); - - $response = (new SocialMediaAssistant( - workspace: $workspace, - post: $post, - userId: $request->user()->id, - ))->prompt($promptWithState); - - $responseContent = $response->text; - $attachments = $collector->all(); - - $generatedIntent = $intent->value; - foreach ($attachments as $attachment) { - if (isset($attachment['type'])) { - $generatedIntent = $attachment['type']; - break; - } - } - - $assistantMessage = $post->aiMessages()->create([ - 'role' => 'assistant', - 'content' => $responseContent, - 'attachments' => $attachments, - 'metadata' => ['intent' => $generatedIntent], - ]); - - return response()->json([ - 'user_message' => $userMessage, - 'assistant_message' => $assistantMessage, - ], Response::HTTP_CREATED); - } catch (Throwable $e) { - Log::error('PostAssistantController error', ['error' => $e->getMessage()]); - - $errorMessage = $e instanceof RuntimeException ? $e->getMessage() : __('assistant.error'); - - $assistantMessage = $post->aiMessages()->create([ - 'role' => 'assistant', - 'content' => $errorMessage, - 'metadata' => ['intent' => $intent->value, 'error' => true], - ]); - - return response()->json([ - 'user_message' => $userMessage, - 'assistant_message' => $assistantMessage, - ], Response::HTTP_CREATED); - } + return response()->json([ + 'user_message' => $userMessage, + 'assistant_message' => $assistantMessage, + ], Response::HTTP_ACCEPTED); } } diff --git a/app/Http/Controllers/App/PostController.php b/app/Http/Controllers/App/PostController.php index d9e86f53..ccba82f6 100644 --- a/app/Http/Controllers/App/PostController.php +++ b/app/Http/Controllers/App/PostController.php @@ -6,6 +6,7 @@ use App\Actions\Post\CreatePost; use App\Actions\Post\DeletePost; +use App\Actions\Post\SyncPostPlatforms; use App\Actions\Post\UpdatePost; use App\Enums\Post\Action as PostAction; use App\Enums\Post\Status as PostStatus; @@ -67,7 +68,7 @@ public function calendar(Request $request): Response|RedirectResponse $this->authorize('view', $workspace); - $tz = $workspace->timezone; + $tz = 'UTC'; $view = $request->input('view', 'week'); $currentDay = $request->input('day') @@ -154,6 +155,10 @@ public function edit(Request $request, Post $post): Response|RedirectResponse abort(404); } + if (in_array($post->status, [PostStatus::Draft, PostStatus::Scheduled, PostStatus::Failed], true)) { + SyncPostPlatforms::execute($post); + } + $post->load(['postPlatforms.socialAccount', 'labels']); $socialAccounts = $workspace->socialAccounts()->active()->get(); $labels = $workspace->labels; diff --git a/app/Http/Controllers/App/WorkspaceController.php b/app/Http/Controllers/App/WorkspaceController.php index b92b027d..dd37f4f9 100644 --- a/app/Http/Controllers/App/WorkspaceController.php +++ b/app/Http/Controllers/App/WorkspaceController.php @@ -4,15 +4,22 @@ namespace App\Http\Controllers\App; +use App\Actions\Ai\AutofillBrand; use App\Actions\Workspace\CreateWorkspace; use App\Actions\Workspace\DeleteWorkspace; use App\Http\Requests\App\Workspace\StoreWorkspaceRequest; use App\Http\Requests\App\Workspace\UpdateWorkspaceRequest; use App\Models\Workspace; +use App\Services\Brand\LogoAttacher; +use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; use Inertia\Inertia; use Inertia\Response; +use RuntimeException; +use Symfony\Component\HttpFoundation\Response as SymfonyResponse; +use Throwable; class WorkspaceController extends Controller { @@ -46,19 +53,45 @@ public function create(Request $request): Response|RedirectResponse return Inertia::render('workspaces/Create'); } - public function store(StoreWorkspaceRequest $request): RedirectResponse + public function autofillBrand(Request $request, AutofillBrand $autofill): JsonResponse + { + $validated = $request->validate([ + 'url' => ['required', 'string', 'max:255'], + ]); + + try { + $metadata = $autofill(data_get($validated, 'url')); + } catch (RuntimeException $e) { + return response()->json(['message' => $e->getMessage()], SymfonyResponse::HTTP_UNPROCESSABLE_ENTITY); + } + + return response()->json($metadata->toArray()); + } + + public function store(StoreWorkspaceRequest $request, LogoAttacher $logoAttacher): RedirectResponse { $user = $request->user(); - if ($user->ownedWorkspacesCount() > 0 && ! $user->account?->hasActiveSubscription()) { - return redirect()->route('app.billing.index') - ->with('message', 'Subscribe to create more workspaces.'); + $validated = $request->validated(); + $isFirstWorkspace = ! $user->workspaces()->exists(); + + $workspace = CreateWorkspace::execute($user, $validated); + + if ($logoUrl = data_get($validated, 'logo_url')) { + try { + $logoAttacher->attach($workspace, $logoUrl); + } catch (Throwable $e) { + Log::warning('Logo attach failed during workspace creation', [ + 'workspace_id' => $workspace->id, + 'logo_url' => $logoUrl, + 'error' => $e->getMessage(), + ]); + } } - CreateWorkspace::execute($user, $request->validated()); - - return redirect()->route('app.calendar') - ->with('success', 'Workspace created successfully!'); + return $isFirstWorkspace + ? redirect()->route('app.accounts')->with('success', __('workspaces.create.first_workspace_success')) + : redirect()->route('app.calendar')->with('success', __('workspaces.create.success')); } public function switch(Request $request, Workspace $workspace): RedirectResponse @@ -85,10 +118,6 @@ public function settings(Request $request): Response|RedirectResponse $this->authorize('update', $workspace); - $timezones = collect(timezone_identifiers_list()) - ->mapWithKeys(fn ($tz) => [$tz => $tz]) - ->toArray(); - $members = $workspace->members() ->get() ->map(fn ($member) => [ @@ -108,7 +137,6 @@ public function settings(Request $request): Response|RedirectResponse 'workspace' => $workspace, 'members' => $members, 'invitations' => $invitations, - 'timezones' => $timezones, ]); } diff --git a/app/Http/Middleware/App/EnsureAccountReady.php b/app/Http/Middleware/App/EnsureAccountReady.php new file mode 100644 index 00000000..cdb8d4b9 --- /dev/null +++ b/app/Http/Middleware/App/EnsureAccountReady.php @@ -0,0 +1,37 @@ +user(); + + if (! $user) { + return $next($request); + } + + $account = $user->account; + + if (! config('trypost.self_hosted') && (! $account || ! $account->subscribed(Account::SUBSCRIPTION_NAME))) { + return redirect()->route('app.subscribe'); + } + + if (! $user->workspaces()->exists()) { + return redirect()->route('app.workspaces.create'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/App/EnsureSubscribed.php b/app/Http/Middleware/App/EnsureSubscribed.php deleted file mode 100644 index 9a98d526..00000000 --- a/app/Http/Middleware/App/EnsureSubscribed.php +++ /dev/null @@ -1,39 +0,0 @@ -user(); - - if (! $user) { - return redirect()->route('login'); - } - - // Let onboarding middleware handle users still in setup - if ($user->setup !== Setup::Completed) { - return $next($request); - } - - $account = $user->account; - - if ($account && $account->hasActiveSubscription()) { - return $next($request); - } - - return redirect()->route('app.subscribe'); - } -} diff --git a/app/Http/Middleware/App/EnsureUserSetupIsComplete.php b/app/Http/Middleware/App/EnsureUserSetupIsComplete.php deleted file mode 100644 index d3ac03f7..00000000 --- a/app/Http/Middleware/App/EnsureUserSetupIsComplete.php +++ /dev/null @@ -1,55 +0,0 @@ -user(); - - if (! $user) { - return $next($request); - } - - // If setup is completed, allow through - if ($user->setup === Setup::Completed) { - return $next($request); - } - - // Map setup status to allowed routes - $allowedRoutes = match ($user->setup) { - Setup::Role => ['app.onboarding.role', 'app.onboarding.role.store'], - Setup::Connections => ['app.onboarding.account', 'app.onboarding.account.store', 'app.social.*'], - default => ['app.onboarding.role', 'app.onboarding.role.store'], - }; - - $currentRoute = $request->route()?->getName(); - - // Check if current route is allowed - foreach ($allowedRoutes as $pattern) { - if ($currentRoute === $pattern || fnmatch($pattern, $currentRoute ?? '')) { - return $next($request); - } - } - - // Redirect to appropriate step - return match ($user->setup) { - Setup::Role => redirect()->route('app.onboarding.role'), - Setup::Connections => redirect()->route('app.onboarding.account'), - default => redirect()->route('app.onboarding.role'), - }; - } -} diff --git a/app/Http/Requests/App/Onboarding/StoreBrandRequest.php b/app/Http/Requests/App/Onboarding/StoreBrandRequest.php deleted file mode 100644 index 43c9113c..00000000 --- a/app/Http/Requests/App/Onboarding/StoreBrandRequest.php +++ /dev/null @@ -1,26 +0,0 @@ - ['nullable', 'url', 'max:255'], - 'brand_description' => ['nullable', 'string', 'max:2000'], - 'brand_tone' => ['required', 'string', 'in:professional,casual,friendly,bold,inspirational,humorous,educational'], - 'brand_voice_notes' => ['nullable', 'string', 'max:2000'], - 'content_language' => ['required', 'string', 'in:en,pt-BR,es'], - ]; - } -} diff --git a/app/Http/Requests/App/Workspace/StoreWorkspaceRequest.php b/app/Http/Requests/App/Workspace/StoreWorkspaceRequest.php index cd57e0aa..8c2e6dac 100644 --- a/app/Http/Requests/App/Workspace/StoreWorkspaceRequest.php +++ b/app/Http/Requests/App/Workspace/StoreWorkspaceRequest.php @@ -17,6 +17,12 @@ public function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], + 'brand_website' => ['nullable', 'url', 'max:255'], + 'brand_description' => ['nullable', 'string', 'max:2000'], + 'brand_tone' => ['nullable', 'string', 'in:professional,casual,friendly,bold,inspirational,humorous,educational'], + 'brand_voice_notes' => ['nullable', 'string', 'max:2000'], + 'content_language' => ['nullable', 'string', 'in:en,pt-BR,es'], + 'logo_url' => ['nullable', 'url', 'max:1024'], ]; } diff --git a/app/Http/Requests/App/Workspace/UpdateWorkspaceRequest.php b/app/Http/Requests/App/Workspace/UpdateWorkspaceRequest.php index fb2947bb..99533576 100644 --- a/app/Http/Requests/App/Workspace/UpdateWorkspaceRequest.php +++ b/app/Http/Requests/App/Workspace/UpdateWorkspaceRequest.php @@ -4,7 +4,6 @@ namespace App\Http\Requests\App\Workspace; -use App\Rules\Timezone; use Illuminate\Foundation\Http\FormRequest; class UpdateWorkspaceRequest extends FormRequest @@ -18,7 +17,6 @@ public function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], - 'timezone' => ['required', 'string', new Timezone], 'brand_website' => ['nullable', 'url', 'max:255'], 'brand_description' => ['nullable', 'string', 'max:2000'], 'brand_tone' => ['nullable', 'string', 'in:professional,casual,friendly,bold,inspirational,humorous,educational'], @@ -32,8 +30,6 @@ public function messages(): array return [ 'name.required' => 'The workspace name is required.', 'name.max' => 'The workspace name must be at most 255 characters.', - 'timezone.required' => 'Please select a timezone.', - 'timezone.timezone' => 'Please select a valid timezone.', ]; } } diff --git a/app/Http/Resources/Api/WorkspaceResource.php b/app/Http/Resources/Api/WorkspaceResource.php index b8a26f9f..94be3e07 100644 --- a/app/Http/Resources/Api/WorkspaceResource.php +++ b/app/Http/Resources/Api/WorkspaceResource.php @@ -17,7 +17,6 @@ public function toArray(Request $request): array return [ 'id' => $this->id, 'name' => $this->name, - 'timezone' => $this->timezone, 'created_at' => $this->created_at->format('Y-m-d H:i:s'), 'updated_at' => $this->updated_at->format('Y-m-d H:i:s'), ]; diff --git a/app/Http/Resources/App/HandleInertiaRequests/AuthWorkspaceResource.php b/app/Http/Resources/App/HandleInertiaRequests/AuthWorkspaceResource.php index d273ed0d..4413b15a 100644 --- a/app/Http/Resources/App/HandleInertiaRequests/AuthWorkspaceResource.php +++ b/app/Http/Resources/App/HandleInertiaRequests/AuthWorkspaceResource.php @@ -17,7 +17,6 @@ public static function make(Workspace $workspace, ?User $user = null): array return [ 'id' => $workspace->id, 'name' => $workspace->name, - 'timezone' => $workspace->timezone, 'logo_url' => $workspace->logo_url, 'created_at' => $workspace->created_at->toIso8601String(), 'role' => $user ? self::resolveRole($workspace, $user) : null, diff --git a/app/Http/Resources/App/MediaResource.php b/app/Http/Resources/App/MediaResource.php new file mode 100644 index 00000000..ed233edf --- /dev/null +++ b/app/Http/Resources/App/MediaResource.php @@ -0,0 +1,29 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'path' => $this->path, + 'url' => $this->url, + 'type' => $this->type->value, + 'mime_type' => $this->mime_type, + 'original_filename' => $this->original_filename, + 'size' => $this->size, + 'meta' => $this->meta, + 'created_at' => $this->created_at->toISOString(), + ]; + } +} diff --git a/app/Jobs/Ai/GenerateAssistantResponse.php b/app/Jobs/Ai/GenerateAssistantResponse.php new file mode 100644 index 00000000..e3e6b4b6 --- /dev/null +++ b/app/Jobs/Ai/GenerateAssistantResponse.php @@ -0,0 +1,151 @@ +onQueue('ai'); + } + + public function handle(IntentDetector $intentDetector, AttachmentCollector $collector, HumanizerService $humanizer): void + { + $this->assistantMessage->update(['status' => Status::Generating]); + + AssistantMessageUpdated::dispatch($this->assistantMessage); + + $post = $this->assistantMessage->post()->with('postPlatforms')->firstOrFail(); + $workspace = $post->workspace; + + $intent = Intent::tryFrom($this->intent) ?? Intent::Text; + + $assistantMessages = $post->aiMessages() + ->where('role', 'assistant') + ->where('id', '!=', $this->assistantMessage->id) + ->get(); + + $imagesInThread = $assistantMessages->sum( + fn ($m) => collect($m->attachments ?? [])->where('type', 'image')->count() + ); + + $videosInThread = $assistantMessages->sum( + fn ($m) => collect($m->attachments ?? [])->where('type', 'video')->count() + ); + + $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{$this->prompt}"; + + $collector->clear(); + + $response = (new SocialMediaAssistant( + workspace: $workspace, + post: $post, + userId: $this->assistantMessage->user_id, + ))->prompt($promptWithState); + + $responseContent = (string) ($response['message'] ?? $response->text ?? ''); + $quickActions = $response['quick_actions'] ?? []; + + $attachments = $collector->all(); + + // Only humanize actual post captions (turns that generated media). + // Conversational turns (greetings, questions, plan summaries) skip + // the humanizer to preserve the agent's natural short responses. + if (! empty($attachments)) { + $responseContent = $humanizer->humanize($responseContent, $workspace); + } + + $generatedIntent = $intent->value; + foreach ($attachments as $attachment) { + if (isset($attachment['type'])) { + $generatedIntent = $attachment['type']; + break; + } + } + + $this->assistantMessage->update([ + 'content' => $responseContent, + 'attachments' => $attachments, + 'status' => Status::Completed, + 'metadata' => array_merge( + $this->assistantMessage->metadata ?? [], + ['intent' => $generatedIntent, 'quick_actions' => $quickActions], + ), + ]); + + AssistantMessageUpdated::dispatch($this->assistantMessage); + } + + public function failed(?Throwable $exception): void + { + Log::error('GenerateAssistantResponse job failed', [ + 'assistant_message_id' => $this->assistantMessage->id, + 'error' => $exception?->getMessage(), + ]); + + $errorMessage = $exception instanceof RuntimeException + ? $exception->getMessage() + : __('assistant.error'); + + $this->assistantMessage->update([ + 'content' => $errorMessage, + 'status' => Status::Failed, + 'error_message' => $exception?->getMessage(), + 'metadata' => array_merge( + $this->assistantMessage->metadata ?? [], + ['error' => true], + ), + ]); + + AssistantMessageUpdated::dispatch($this->assistantMessage); + } +} diff --git a/app/Mcp/Tools/Workspace/GetWorkspaceTool.php b/app/Mcp/Tools/Workspace/GetWorkspaceTool.php index 994d5b67..91670c6d 100644 --- a/app/Mcp/Tools/Workspace/GetWorkspaceTool.php +++ b/app/Mcp/Tools/Workspace/GetWorkspaceTool.php @@ -12,7 +12,7 @@ use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly; #[IsReadOnly] -#[Description('Get the current workspace details including name and timezone.')] +#[Description('Get the current workspace details including name.')] class GetWorkspaceTool extends Tool { public function handle(Request $request): ResponseFactory diff --git a/app/Models/AiMessage.php b/app/Models/AiMessage.php index ed0bb4c5..90779b57 100644 --- a/app/Models/AiMessage.php +++ b/app/Models/AiMessage.php @@ -4,6 +4,7 @@ namespace App\Models; +use App\Enums\AiMessage\Status; use Database\Factories\AiMessageFactory; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Concerns\HasUuids; @@ -23,6 +24,8 @@ class AiMessage extends Model 'role', 'content', 'attachments', + 'status', + 'error_message', 'metadata', ]; @@ -31,6 +34,7 @@ protected function casts(): array return [ 'attachments' => 'array', 'metadata' => 'array', + 'status' => Status::class, ]; } diff --git a/app/Models/User.php b/app/Models/User.php index 226375d5..e0af057c 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,8 +5,6 @@ namespace App\Models; use App\Enums\Notification\Type as NotificationType; -use App\Enums\User\Persona; -use App\Enums\User\Setup; use App\Models\Traits\HasMedia; use App\Models\Traits\HasWorkspace; use Database\Factories\UserFactory; @@ -31,8 +29,6 @@ class User extends Authenticatable implements MustVerifyEmail 'name', 'email', 'password', - 'setup', - 'persona', 'account_id', 'current_workspace_id', 'email_verified_at', @@ -69,8 +65,6 @@ protected function casts(): array 'email_verified_at' => 'datetime', 'password' => 'hashed', 'two_factor_confirmed_at' => 'datetime', - 'setup' => Setup::class, - 'persona' => Persona::class, ]; } diff --git a/app/Models/Workspace.php b/app/Models/Workspace.php index e384d56a..93cd009b 100644 --- a/app/Models/Workspace.php +++ b/app/Models/Workspace.php @@ -23,7 +23,6 @@ class Workspace extends Model 'account_id', 'user_id', 'name', - 'timezone', 'brand_website', 'brand_description', 'brand_tone', diff --git a/app/Services/Ai/HumanizerService.php b/app/Services/Ai/HumanizerService.php new file mode 100644 index 00000000..5cfc3503 --- /dev/null +++ b/app/Services/Ai/HumanizerService.php @@ -0,0 +1,42 @@ + $workspace->name, + 'brand_tone' => $workspace->brand_tone, + 'brand_voice_notes' => $workspace->brand_voice_notes, + 'content_language' => $workspace->content_language, + ])->render(); + + try { + $response = (new Humanizer($instructions))->prompt($text); + + $rewritten = trim((string) $response->text); + + return $rewritten !== '' ? $rewritten : $text; + } catch (Throwable $e) { + Log::warning('Humanizer pass failed; returning original text.', [ + 'workspace_id' => $workspace->id, + 'error' => $e->getMessage(), + ]); + + return $text; + } + } +} diff --git a/app/Services/Ai/VideoGenerationService.php b/app/Services/Ai/VideoGenerationService.php index 10aea132..ef2190c9 100644 --- a/app/Services/Ai/VideoGenerationService.php +++ b/app/Services/Ai/VideoGenerationService.php @@ -20,7 +20,7 @@ class VideoGenerationService private string $model = 'veo-3.1-generate-preview'; - private int $maxPollAttempts = 30; + private int $maxPollAttempts = 60; private int $pollIntervalSeconds = 10; diff --git a/app/Services/Brand/BrandAnalyzerRunner.php b/app/Services/Brand/BrandAnalyzerRunner.php new file mode 100644 index 00000000..6917acba --- /dev/null +++ b/app/Services/Brand/BrandAnalyzerRunner.php @@ -0,0 +1,55 @@ + ! empty(config('services.openai.api_key')), + 'gemini' => ! empty(config('services.gemini.api_key')), + default => false, + }; + } + + public function analyze(string $bodyHtml): ?LlmBrandAnalysis + { + $markdown = $this->htmlToMarkdown($bodyHtml); + + if ($markdown === '') { + return null; + } + + try { + $response = (new BrandAnalyzer)->prompt($markdown); + } catch (Throwable $e) { + Log::warning('BrandAnalyzer failed, falling back to meta tags', ['error' => $e->getMessage()]); + + return null; + } + + return LlmBrandAnalysis::fromResponse($response); + } + + private function htmlToMarkdown(string $html): string + { + if (trim($html) === '') { + return ''; + } + + $markdown = (new HtmlConverter(['strip_tags' => true]))->convert($html); + + return Str::limit(trim($markdown), self::MARKDOWN_MAX_CHARS, ''); + } +} diff --git a/app/Services/Brand/BrandMetadata.php b/app/Services/Brand/BrandMetadata.php new file mode 100644 index 00000000..f5697706 --- /dev/null +++ b/app/Services/Brand/BrandMetadata.php @@ -0,0 +1,56 @@ +name ?: $this->name, + description: $llm->description ?: $this->description, + language: $llm->language ?: $this->language, + tone: $llm->tone ?: null, + voiceNotes: $llm->voiceNotes ?: null, + logoUrl: $this->logoUrl, + ); + } + + public function withLogoUrl(?string $logoUrl): self + { + return new self( + name: $this->name, + description: $this->description, + language: $this->language, + tone: $this->tone, + voiceNotes: $this->voiceNotes, + logoUrl: $logoUrl, + ); + } + + /** + * @return array{name: ?string, brand_description: ?string, content_language: ?string, brand_tone: ?string, brand_voice_notes: ?string, logo_url: ?string} + */ + public function toArray(): array + { + return [ + 'name' => $this->name, + 'brand_description' => $this->description, + 'content_language' => $this->language, + 'brand_tone' => $this->tone, + 'brand_voice_notes' => $this->voiceNotes, + 'logo_url' => $this->logoUrl, + ]; + } +} diff --git a/app/Services/Brand/HomepageMetaExtractor.php b/app/Services/Brand/HomepageMetaExtractor.php new file mode 100644 index 00000000..04eeb0e0 --- /dev/null +++ b/app/Services/Brand/HomepageMetaExtractor.php @@ -0,0 +1,245 @@ + elements — does not call any LLM. + */ +final class HomepageMetaExtractor +{ + private const array TITLE_SEPARATORS = [' | ', ' - ', ' — ', ' – ']; + + public function extract(string $html, string $baseUrl): BrandMetadata + { + $crawler = new Crawler($html, $baseUrl); + + return new BrandMetadata( + name: $this->extractName($crawler, $baseUrl), + description: $this->extractDescription($crawler), + language: $this->extractLanguage($crawler), + logoUrl: $this->extractLogoUrl($crawler, $baseUrl), + ); + } + + /** + * Body content converted for the LLM. Returns the main element when available, + * falling back to body, with script/style/nav/footer stripped. + */ + public function extractBodyHtml(string $html): string + { + $crawler = new Crawler($html); + $body = $crawler->filter('main')->first(); + + if ($body->count() === 0) { + $body = $crawler->filter('body')->first(); + } + + if ($body->count() === 0) { + return ''; + } + + foreach (['script', 'style', 'nav', 'footer', 'noscript'] as $selector) { + $body->filter($selector)->each(function (Crawler $node): void { + $domNode = $node->getNode(0); + $domNode?->parentNode?->removeChild($domNode); + }); + } + + return $body->html(); + } + + private function extractName(Crawler $crawler, string $baseUrl): ?string + { + $ogSiteName = $this->metaContent($crawler, 'property', 'og:site_name'); + if ($ogSiteName !== null) { + return $ogSiteName; + } + + $domainName = $this->extractDomainName($baseUrl); + + $ogTitle = $this->metaContent($crawler, 'property', 'og:title'); + if ($ogTitle !== null) { + return $this->pickBrandName($ogTitle, $domainName); + } + + $title = $crawler->filter('title')->first(); + if ($title->count() > 0) { + $titleText = trim($title->text('')); + if ($titleText !== '') { + return $this->pickBrandName($titleText, $domainName); + } + } + + return $domainName; + } + + /** + * Pick the cleanest brand name from a page title. + * + * If the title contains a separator (e.g. "Acme | Tagline"), strip the suffix and use + * the prefix as the brand name. If there's no separator, the title is likely a tagline + * with no brand prefix (e.g. "Email API, SMTP & Marketing Platform"), so we fall back + * to the domain-derived name (e.g. "Sendkit") which is a much better default for the + * workspace name. + */ + private function pickBrandName(string $title, ?string $domainName): ?string + { + $stripped = $this->stripTitleSuffix($title); + + if ($stripped !== $title && $stripped !== '') { + return $stripped; + } + + return $domainName ?? ($stripped !== '' ? $stripped : null); + } + + /** + * Derive a brand name from the URL host. + * + * Examples: sendkit.dev → "Sendkit", www.acme.com → "Acme", acme.co.uk → "Acme". + * Edge case: subdomain hosts like blog.acme.com return "Blog" — acceptable since users + * typically enter their apex domain when registering a workspace. + */ + private function extractDomainName(string $url): ?string + { + $host = parse_url($url, PHP_URL_HOST); + + if (! is_string($host) || $host === '') { + return null; + } + + $host = preg_replace('/^www\./i', '', $host); + $first = explode('.', (string) $host)[0] ?? ''; + + if ($first === '') { + return null; + } + + return ucfirst(strtolower($first)); + } + + private function extractDescription(Crawler $crawler): ?string + { + return $this->metaContent($crawler, 'name', 'description') + ?? $this->metaContent($crawler, 'property', 'og:description'); + } + + private function extractLanguage(Crawler $crawler): ?string + { + $html = $crawler->filter('html')->first(); + + if ($html->count() === 0) { + return null; + } + + $lang = trim((string) $html->attr('lang', '')); + + if ($lang === '') { + return null; + } + + $lower = strtolower($lang); + + return match (true) { + str_starts_with($lower, 'pt') => 'pt-BR', + str_starts_with($lower, 'es') => 'es', + str_starts_with($lower, 'en') => 'en', + default => null, + }; + } + + private function extractLogoUrl(Crawler $crawler, string $baseUrl): ?string + { + $candidates = []; + + // Prefer modern PWA icons (rel="icon" with explicit large sizes — android-chrome-*, + // favicon-512x512 etc.) because they're usually the cleanest full-logo version. + // apple-touch-icon is a strong second (always 180x180). The classic /favicon.ico + // lives inside rel*="icon" but loses the tiebreak on size. og:image is intentionally + // excluded because it is almost always a social preview banner, not the logo. + foreach ($crawler->filter('link[rel*="icon"]') as $node) { + $href = (string) $node->getAttribute('href'); + if ($href === '') { + continue; + } + + $rel = strtolower((string) $node->getAttribute('rel')); + $candidates[] = [ + 'href' => $href, + 'size' => $this->parseIconSize($node->getAttribute('sizes')), + 'apple' => str_contains($rel, 'apple-touch-icon') ? 1 : 0, + ]; + } + + if ($candidates === []) { + // Fall back to the classic /favicon.ico convention — some sites don't declare + // any at all but still serve one at the well-known path. + $origin = $this->originFromUrl($baseUrl); + if ($origin !== null) { + return $origin.'/favicon.ico'; + } + + return null; + } + + // Biggest declared size wins; apple-touch-icon breaks ties over bare "icon". + usort($candidates, fn (array $a, array $b) => $b['size'] <=> $a['size'] ?: $b['apple'] <=> $a['apple']); + + return UriResolver::resolve((string) data_get($candidates, '0.href'), $baseUrl); + } + + private function originFromUrl(string $url): ?string + { + $parts = parse_url($url); + $scheme = (string) data_get($parts, 'scheme', ''); + $host = (string) data_get($parts, 'host', ''); + + if ($scheme === '' || $host === '') { + return null; + } + + return "{$scheme}://{$host}"; + } + + private function parseIconSize(?string $sizes): int + { + if ($sizes === null || $sizes === '') { + return 0; + } + + preg_match_all('/(\d+)x\d+/', $sizes, $matches); + + return $matches[1] === [] ? 0 : max(array_map('intval', $matches[1])); + } + + private function metaContent(Crawler $crawler, string $attr, string $value): ?string + { + $node = $crawler->filter(sprintf('meta[%s="%s"]', $attr, $value))->first(); + + if ($node->count() === 0) { + return null; + } + + $content = trim((string) $node->attr('content', '')); + + return $content === '' ? null : $content; + } + + private function stripTitleSuffix(string $title): string + { + foreach (self::TITLE_SEPARATORS as $sep) { + $idx = mb_strpos($title, $sep); + if ($idx !== false && $idx > 0) { + return trim(mb_substr($title, 0, $idx)); + } + } + + return trim($title); + } +} diff --git a/app/Services/Brand/LlmBrandAnalysis.php b/app/Services/Brand/LlmBrandAnalysis.php new file mode 100644 index 00000000..ab12b742 --- /dev/null +++ b/app/Services/Brand/LlmBrandAnalysis.php @@ -0,0 +1,29 @@ +fetcher->tryGet($logoUrl); + + if ($response === null) { + Log::debug('Logo fetch failed', ['url' => $logoUrl]); + + return false; + } + + $contentType = $this->parseMimeType($response->header('Content-Type')); + + if (! in_array($contentType, self::ALLOWED_MIMES, true)) { + Log::debug('Logo rejected — disallowed mime', ['url' => $logoUrl, 'mime' => $contentType]); + + return false; + } + + $declaredLength = (int) $response->header('Content-Length'); + + if ($declaredLength > self::MAX_LOGO_BYTES) { + Log::debug('Logo rejected — content-length too large', ['url' => $logoUrl, 'bytes' => $declaredLength]); + + return false; + } + + $body = $response->body(); + + if (strlen($body) > self::MAX_LOGO_BYTES) { + Log::debug('Logo rejected — body too large', ['url' => $logoUrl, 'bytes' => strlen($body)]); + + return false; + } + + $tempPath = $this->writeToTempFile($body, $this->extensionForMime($contentType)); + + try { + $workspace->clearMediaCollection('logo'); + $workspace->addMediaFromPath( + $tempPath, + 'logo.'.$this->extensionForMime($contentType), + 'logo', + ['ai_autofill' => true], + ); + + return true; + } finally { + if (file_exists($tempPath)) { + @unlink($tempPath); + } + } + } + + private function writeToTempFile(string $body, string $extension): string + { + $base = tempnam(sys_get_temp_dir(), 'logo_'); + + // tempnam() creates an empty file without extension. Add the extension + // by renaming so we don't leave the extension-less file behind. + $withExtension = $base.'.'.$extension; + rename($base, $withExtension); + + file_put_contents($withExtension, $body); + + return $withExtension; + } + + private function parseMimeType(?string $header): string + { + if ($header === null || $header === '') { + return ''; + } + + $first = explode(';', $header)[0]; + + return strtolower(trim($first)); + } + + private function extensionForMime(string $mime): string + { + return match ($mime) { + 'image/jpeg' => 'jpg', + 'image/webp' => 'webp', + 'image/gif' => 'gif', + 'image/x-icon', 'image/vnd.microsoft.icon' => 'ico', + default => 'png', + }; + } +} diff --git a/app/Services/Brand/SafeHttpFetcher.php b/app/Services/Brand/SafeHttpFetcher.php new file mode 100644 index 00000000..667146f6 --- /dev/null +++ b/app/Services/Brand/SafeHttpFetcher.php @@ -0,0 +1,99 @@ +guardAgainstSsrf($url); + + try { + $response = Http::timeout(self::TIMEOUT_SECONDS) + ->withUserAgent(self::USER_AGENT) + ->withOptions(['allow_redirects' => ['max' => self::MAX_REDIRECTS]]) + ->get($url); + } catch (ConnectionException $e) { + throw new RuntimeException(__('workspaces.create.autofill_errors.unreachable', ['reason' => $e->getMessage()])); + } + + if ($response->failed()) { + throw new RuntimeException(__('workspaces.create.autofill_errors.http_status', ['status' => $response->status()])); + } + + return $response; + } + + /** + * Same as get() but never throws — returns null on any failure. Used for logo + * downloads and other opportunistic fetches where failure is not fatal. + */ + public function tryGet(string $url): ?Response + { + try { + return $this->get($url); + } catch (RuntimeException) { + return null; + } + } + + public function guardAgainstSsrf(string $url): void + { + $parts = parse_url($url); + $scheme = strtolower((string) data_get($parts, 'scheme', '')); + + if (! in_array($scheme, ['http', 'https'], true)) { + throw new RuntimeException(__('workspaces.create.autofill_errors.invalid_scheme')); + } + + $host = (string) data_get($parts, 'host', ''); + + if ($host === '') { + throw new RuntimeException(__('workspaces.create.autofill_errors.missing_host')); + } + + $ip = gethostbyname($host); + + if ($ip === $host && filter_var($host, FILTER_VALIDATE_IP) === false) { + throw new RuntimeException(__('workspaces.create.autofill_errors.unresolvable_host', ['host' => $host])); + } + + $isPublic = filter_var( + $ip, + FILTER_VALIDATE_IP, + FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE, + ); + + if ($isPublic === false) { + throw new RuntimeException(__('workspaces.create.autofill_errors.private_network')); + } + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 62d1c155..1a5ecc75 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,7 +1,6 @@ alias([ - 'subscribed' => EnsureSubscribed::class, 'api.auth' => AuthenticateApiToken::class, 'mcp.auth' => AuthenticateMcpToken::class, ]); diff --git a/config/horizon.php b/config/horizon.php index a5ba8e0d..3071d096 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -240,6 +240,21 @@ 'tries' => 1, 'nice' => 0, ], + + 'ai-assistant' => [ + 'connection' => 'redis', + 'queue' => ['ai'], + 'balance' => 'auto', + 'autoScalingStrategy' => 'time', + 'minProcesses' => 1, + 'maxProcesses' => 2, + 'timeout' => 930, + 'maxTime' => 0, + 'maxJobs' => 0, + 'memory' => 512, + 'tries' => 1, + 'nice' => 0, + ], ], 'environments' => [ @@ -255,6 +270,12 @@ 'balanceMaxShift' => 1, 'balanceCooldown' => 3, ], + + 'ai-assistant' => [ + 'maxProcesses' => 5, + 'balanceMaxShift' => 1, + 'balanceCooldown' => 3, + ], ], 'local' => [ @@ -265,6 +286,10 @@ 'social-publishing' => [ 'maxProcesses' => 3, ], + + 'ai-assistant' => [ + 'maxProcesses' => 2, + ], ], ], ]; diff --git a/database/factories/WorkspaceFactory.php b/database/factories/WorkspaceFactory.php index 2eaa58ba..6b8ca431 100644 --- a/database/factories/WorkspaceFactory.php +++ b/database/factories/WorkspaceFactory.php @@ -24,7 +24,6 @@ public function definition(): array return [ 'user_id' => User::factory(), 'name' => fake()->company(), - 'timezone' => fake()->timezone(), ]; } diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 41a415ae..70499acd 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -30,8 +30,6 @@ public function up(): void $table->text('two_factor_recovery_codes')->nullable(); $table->timestamp('two_factor_confirmed_at')->nullable(); $table->rememberToken(); - $table->string('setup')->nullable(); - $table->string('persona')->nullable(); $table->uuid('account_id')->nullable(); $table->uuid('current_workspace_id')->nullable(); $table->uuid('language_id')->nullable(); diff --git a/database/migrations/2026_01_14_232315_create_workspaces_table.php b/database/migrations/2026_01_14_232315_create_workspaces_table.php index d442c243..c39ffb0c 100644 --- a/database/migrations/2026_01_14_232315_create_workspaces_table.php +++ b/database/migrations/2026_01_14_232315_create_workspaces_table.php @@ -18,7 +18,6 @@ public function up(): void $table->foreignUuid('account_id')->constrained()->cascadeOnDelete(); $table->uuid('user_id')->nullable(); $table->string('name'); - $table->string('timezone'); $table->string('brand_website')->nullable(); $table->text('brand_description')->nullable(); $table->string('brand_tone')->default('professional'); diff --git a/database/migrations/2026_04_16_155126_add_status_to_ai_messages_table.php b/database/migrations/2026_04_16_155126_add_status_to_ai_messages_table.php new file mode 100644 index 00000000..0a6aee2c --- /dev/null +++ b/database/migrations/2026_04_16_155126_add_status_to_ai_messages_table.php @@ -0,0 +1,25 @@ +string('status')->default('completed')->after('attachments'); + $table->text('error_message')->nullable()->after('status'); + }); + } + + public function down(): void + { + Schema::table('ai_messages', function (Blueprint $table) { + $table->dropColumn(['status', 'error_message']); + }); + } +}; diff --git a/lang/en/assistant.php b/lang/en/assistant.php index 6415d5e0..039848b3 100644 --- a/lang/en/assistant.php +++ b/lang/en/assistant.php @@ -1,15 +1,16 @@ 'Ask me to write a caption, generate an image, create audio...', + 'placeholder' => 'Ask me to write a caption, generate an image or video...', 'thinking' => 'Thinking...', 'add_to_post' => 'Add to post', 'added' => 'Added', 'error' => 'Something went wrong. Please try again.', + 'retry' => 'Try again', 'image_generated' => 'Here is the generated image:', 'video_generated' => 'Here is the generated video:', 'audio_generated' => 'Here is the generated audio:', - 'empty' => 'Ask me anything. I can write captions, generate images, create audio, and produce videos.', + 'empty' => 'Ask me anything. I can write captions, generate images, 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/en/onboarding.php b/lang/en/onboarding.php deleted file mode 100644 index d6727687..00000000 --- a/lang/en/onboarding.php +++ /dev/null @@ -1,78 +0,0 @@ - [ - 'page_title' => 'Welcome - Tell us about yourself', - 'title' => 'Tell us about yourself', - 'description' => 'Help us personalize your experience', - 'submit' => 'Continue', - ], - - 'personas' => [ - 'founder' => [ - 'label' => 'Founder', - 'description' => 'Building a startup or new venture', - ], - 'creator' => [ - 'label' => 'Creator', - 'description' => 'Content creator or influencer', - ], - 'agency' => [ - 'label' => 'Agency', - 'description' => 'Marketing or social media agency', - ], - 'enterprise' => [ - 'label' => 'Enterprise', - 'description' => 'Large company or corporation', - ], - 'small_business' => [ - 'label' => 'Small Business', - 'description' => 'Small to medium business', - ], - 'personal' => [ - 'label' => 'Personal', - 'description' => 'Personal brand or hobby', - ], - ], - - 'brand' => [ - 'page_title' => 'Tell us about your brand', - 'title' => 'Tell us about your brand', - 'description' => 'These defaults power every AI-generated post in this workspace. You can change them later in Settings.', - 'submit' => 'Continue', - 'skip' => 'Skip for now', - 'autofill' => 'Autofill', - 'autofill_success' => 'We pulled what we could from your site. Review and adjust if needed.', - 'autofill_error' => 'We could not read that site. Please fill in manually.', - 'autofill_missing_url' => 'Add your website URL first.', - 'logo_captured' => 'Logo captured from your site.', - - 'website' => 'Website', - 'website_placeholder' => 'https://yourbrand.com', - 'brand_description' => 'Description', - 'brand_description_placeholder' => 'Tell us about your brand, what you do, and who your audience is...', - 'tone' => 'Tone of voice', - 'tone_professional' => 'Professional', - 'tone_casual' => 'Casual', - 'tone_friendly' => 'Friendly', - 'tone_bold' => 'Bold', - 'tone_inspirational' => 'Inspirational', - 'tone_humorous' => 'Humorous', - 'tone_educational' => 'Educational', - 'voice_notes' => 'Voice notes', - 'voice_notes_placeholder' => 'Additional writing guidelines, words to avoid, style preferences...', - 'content_language' => 'Content language', - 'content_language_description' => 'Language used for AI-generated captions, hashtags, and any text inside generated images or videos.', - ], - - 'connect' => [ - 'page_title' => 'Connect your accounts', - 'title' => 'Connect your accounts', - 'description' => 'Connect at least one social network to get started', - 'submit' => 'Continue', - 'error' => 'Something went wrong. Please try again.', - 'go_back' => 'Go Back', - ], -]; diff --git a/lang/en/posts.php b/lang/en/posts.php index b227cdff..7942123d 100644 --- a/lang/en/posts.php +++ b/lang/en/posts.php @@ -83,14 +83,21 @@ 'schedule' => 'Schedule', 'pick_time' => 'Pick time', 'post_now' => 'Post now', + 'time' => 'Time', + 'cancel' => 'Cancel', 'delete' => 'Delete', 'schedule_for' => 'Schedule for', + 'scheduled_for' => 'Scheduled for', 'schedule_date' => 'Schedule date', 'saving' => 'Saving...', 'saved' => 'Saved', + 'draft' => 'Draft', 'media' => 'Media', + 'add_media' => 'Add media', 'caption' => 'Caption', 'caption_placeholder' => 'Write your caption...', + 'compose_title' => 'Create a post', + 'compose_subtitle' => 'Compose your message and add media', 'drag_drop' => 'Drag & drop or click to upload', 'publish_to' => 'Publish to', 'organize' => 'Organize', diff --git a/lang/en/settings.php b/lang/en/settings.php index 59601a33..489527d0 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -75,12 +75,11 @@ 'logo_heading' => 'Workspace logo', 'logo_description' => 'Upload a logo for your workspace', 'heading' => 'Workspace name', - 'description' => 'Update your workspace name and timezone', + 'description' => 'Update your workspace name', 'members_heading' => 'Members', 'members_description' => 'Manage workspace members and invitations', 'name' => 'Name', 'name_placeholder' => 'My Workspace', - 'timezone' => 'Timezone', 'save' => 'Save', ], diff --git a/lang/en/workspaces.php b/lang/en/workspaces.php index 23f16154..49e8b2a0 100644 --- a/lang/en/workspaces.php +++ b/lang/en/workspaces.php @@ -11,10 +11,42 @@ 'posts' => ':count posts', 'create' => [ - 'title' => 'Create a workspace', - 'description' => 'Give your workspace a name to get started', + 'page_title' => 'Create your workspace', + 'title' => 'Set up your workspace', + 'description' => 'Tell us about your brand. We\'ll use this to tailor AI-generated posts to your voice.', + 'website' => 'Website', + 'website_placeholder' => 'https://yourbrand.com', + 'autofill' => 'Autofill from website', + 'autofill_missing_url' => 'Enter a URL first.', + 'autofill_success' => 'Brand info loaded.', + 'autofill_error' => 'Could not autofill. You can fill the fields manually.', + 'autofill_errors' => [ + 'unreachable' => 'We could not reach that website (:reason).', + 'http_status' => 'The website returned an unexpected status (:status).', + 'invalid_scheme' => 'Only http and https URLs are supported.', + 'missing_host' => 'The URL is missing a host.', + 'unresolvable_host' => 'We could not resolve the host (:host).', + 'private_network' => 'URLs pointing to private networks are not allowed.', + ], + 'logo_captured' => 'Logo captured from your website.', 'name' => 'Workspace name', - 'name_placeholder' => 'My Workspace', + 'name_placeholder' => 'e.g. Acme Inc', + 'brand_description' => 'Brand description', + 'brand_description_placeholder' => 'What does your brand do?', + 'tone' => 'Brand tone', + 'tone_professional' => 'Professional', + 'tone_casual' => 'Casual', + 'tone_friendly' => 'Friendly', + 'tone_bold' => 'Bold', + 'tone_inspirational' => 'Inspirational', + 'tone_humorous' => 'Humorous', + 'tone_educational' => 'Educational', + 'content_language' => 'Content language', + 'content_language_description' => 'AI-generated captions will be written in this language.', + 'voice_notes' => 'Voice notes (optional)', + 'voice_notes_placeholder' => 'e.g. short, punchy sentences. avoid jargon.', 'submit' => 'Create workspace', + 'first_workspace_success' => 'Workspace created. Connect a social account to start posting.', + 'success' => 'Workspace created.', ], ]; diff --git a/lang/es/assets.php b/lang/es/assets.php new file mode 100644 index 00000000..555cf03c --- /dev/null +++ b/lang/es/assets.php @@ -0,0 +1,51 @@ + 'Medios', + + 'tabs' => [ + 'my_uploads' => 'Mis subidas', + 'stock_photos' => 'Fotos gratuitas', + 'gifs' => 'GIFs', + ], + + 'upload' => [ + 'drag_drop' => 'Arrastra y suelta tus archivos aquí o haz clic para seleccionar', + 'formats' => 'JPEG, PNG, GIF, WebP, MP4', + 'uploading' => 'Subiendo...', + ], + + 'empty' => [ + 'title' => 'Todavía no hay medios', + 'description' => 'Sube imágenes y videos para construir tu biblioteca de medios.', + ], + + 'save_to_assets' => 'Guardar en la biblioteca', + 'saved' => '¡Guardado en tu biblioteca!', + 'create_post' => 'Crear post', + + 'delete' => [ + 'title' => 'Eliminar medio', + 'description' => '¿Estás seguro de que deseas eliminar este medio? Esta acción no se puede deshacer.', + 'confirm' => 'Eliminar', + 'cancel' => 'Cancelar', + ], + + 'unsplash' => [ + 'search_placeholder' => 'Buscar fotos gratuitas...', + 'no_results' => 'No se encontraron fotos', + 'no_results_description' => 'Prueba con otro término de búsqueda.', + 'trending' => 'Tendencias en Unsplash', + 'start_searching' => 'Busca fotos gratuitas de Unsplash', + ], + + 'giphy' => [ + 'trending' => 'Tendencias en Giphy', + 'search_placeholder' => 'Buscar GIFs...', + 'no_results' => 'No se encontraron GIFs', + 'no_results_description' => 'Prueba con otro término de búsqueda.', + 'powered_by' => 'Powered by GIPHY', + ], +]; diff --git a/lang/es/assistant.php b/lang/es/assistant.php new file mode 100644 index 00000000..d1fed14a --- /dev/null +++ b/lang/es/assistant.php @@ -0,0 +1,19 @@ + 'Pídeme escribir una descripción, generar una imagen o video...', + 'thinking' => 'Pensando...', + 'add_to_post' => 'Añadir al post', + 'added' => 'Añadido', + 'error' => 'Algo salió mal. Inténtalo de nuevo.', + 'retry' => 'Intentar de nuevo', + 'image_generated' => 'Aquí está la imagen generada:', + 'video_generated' => 'Aquí está el video generado:', + 'audio_generated' => 'Aquí está el audio generado:', + 'empty' => 'Pregúntame lo que quieras. Puedo escribir descripciones, generar imágenes y producir videos.', + 'limit_reached_images' => 'Has alcanzado el límite mensual de generación de imágenes.', + 'limit_reached_videos' => 'Has alcanzado el límite mensual de generación de videos.', + 'content_blocked' => 'No puedo ayudar con ese tipo de contenido. Estoy aquí para ayudarte a crear contenido seguro y atractivo para redes sociales.', +]; diff --git a/lang/es/comments.php b/lang/es/comments.php new file mode 100644 index 00000000..625fcaca --- /dev/null +++ b/lang/es/comments.php @@ -0,0 +1,18 @@ + 'Escribe un comentario...', + 'reply_placeholder' => 'Escribe una respuesta...', + 'reply' => 'Responder', + 'edit' => 'Editar', + 'delete' => 'Eliminar', + 'edited' => 'editado', + 'save' => 'Guardar', + 'cancel' => 'Cancelar', + 'send' => 'Enviar', + 'replying_to' => 'Respondiendo a :name', + 'empty' => 'Todavía no hay comentarios. Inicia la conversación.', + 'load_more' => 'Cargar comentarios anteriores', +]; diff --git a/lang/es/onboarding.php b/lang/es/onboarding.php deleted file mode 100644 index 67f2e43c..00000000 --- a/lang/es/onboarding.php +++ /dev/null @@ -1,78 +0,0 @@ - [ - 'page_title' => 'Bienvenido - Cuéntanos sobre ti', - 'title' => 'Cuéntanos sobre ti', - 'description' => 'Ayúdanos a personalizar tu experiencia', - 'submit' => 'Continuar', - ], - - 'personas' => [ - 'founder' => [ - 'label' => 'Fundador', - 'description' => 'Construyendo una startup o nuevo negocio', - ], - 'creator' => [ - 'label' => 'Creador de Contenido', - 'description' => 'Creador de contenido o influencer', - ], - 'agency' => [ - 'label' => 'Agencia', - 'description' => 'Agencia de marketing o redes sociales', - ], - 'enterprise' => [ - 'label' => 'Empresa', - 'description' => 'Gran empresa o corporación', - ], - 'small_business' => [ - 'label' => 'Pequeño Negocio', - 'description' => 'Pequeña o mediana empresa', - ], - 'personal' => [ - 'label' => 'Personal', - 'description' => 'Marca personal o hobby', - ], - ], - - 'brand' => [ - 'page_title' => 'Cuéntanos sobre tu marca', - 'title' => 'Cuéntanos sobre tu marca', - 'description' => 'Estos valores predeterminados se usarán en todos los posts generados por IA en este workspace. Puedes cambiarlos después en Configuración.', - 'submit' => 'Continuar', - 'skip' => 'Omitir por ahora', - 'autofill' => 'Autocompletar', - 'autofill_success' => 'Extrajimos lo que pudimos de tu sitio. Revisa y ajusta si es necesario.', - 'autofill_error' => 'No pudimos leer ese sitio. Complétalo manualmente.', - 'autofill_missing_url' => 'Ingresa primero la URL de tu sitio.', - 'logo_captured' => 'Logo capturado desde tu sitio.', - - 'website' => 'Sitio web', - 'website_placeholder' => 'https://tumarca.com', - 'brand_description' => 'Descripción', - 'brand_description_placeholder' => 'Cuéntanos sobre tu marca, lo que haces y quién es tu audiencia...', - 'tone' => 'Tono de voz', - 'tone_professional' => 'Profesional', - 'tone_casual' => 'Casual', - 'tone_friendly' => 'Amigable', - 'tone_bold' => 'Audaz', - 'tone_inspirational' => 'Inspirador', - 'tone_humorous' => 'Humorístico', - 'tone_educational' => 'Educativo', - 'voice_notes' => 'Notas de voz', - 'voice_notes_placeholder' => 'Directrices adicionales de escritura, palabras a evitar, preferencias de estilo...', - 'content_language' => 'Idioma del contenido', - 'content_language_description' => 'Idioma usado en los subtítulos, hashtags y cualquier texto dentro de imágenes o videos generados por IA.', - ], - - 'connect' => [ - 'page_title' => 'Conecta tus cuentas', - 'title' => 'Conecta tus cuentas', - 'description' => 'Conecta al menos una red social para comenzar', - 'submit' => 'Continuar', - 'error' => 'Algo salió mal. Inténtalo de nuevo.', - 'go_back' => 'Volver', - ], -]; diff --git a/lang/es/posts.php b/lang/es/posts.php index 9086fed0..2802bc0d 100644 --- a/lang/es/posts.php +++ b/lang/es/posts.php @@ -87,14 +87,46 @@ 'delete' => 'Eliminar', 'settings' => 'Configuración', 'schedule_for' => 'Programar para', + 'scheduled_for' => 'Programado para', 'saving' => 'Guardando...', 'saved' => 'Guardado', + 'draft' => 'Borrador', 'scheduled_at' => 'Programado:', 'published_at' => 'Publicado:', 'media' => 'Multimedia', + 'add_media' => 'Añadir media', 'caption' => 'Descripción', + 'caption_placeholder' => 'Escribe tu descripción...', + 'compose_title' => 'Crear un post', + 'compose_subtitle' => 'Compón tu mensaje y agrega media', + 'drag_drop' => 'Arrastra y suelta o haz clic para subir', + 'publish_to' => 'Publicar en', + 'organize' => 'Organizar', 'no_caption' => 'Sin descripción', 'no_content' => 'Sin contenido', + 'no_labels' => 'Todavía no hay etiquetas creadas', + 'pick_time' => 'Elegir hora', + 'post_now' => 'Publicar ahora', + 'time' => 'Hora', + 'cancel' => 'Cancelar', + 'schedule_date' => 'Fecha de programación', + 'view_on_platform' => 'Ver en la plataforma', + 'platform_status' => 'Estado de la plataforma', + + 'tabs' => [ + 'preview' => 'Vista previa', + 'schedule' => 'Programación', + 'comments' => 'Comentarios', + 'comments_empty' => 'Todavía no hay comentarios.', + 'writing_assistant' => 'Asistente IA', + 'writing_assistant_empty' => 'Asistente de escritura próximamente.', + ], + + 'status' => [ + 'published' => 'Publicado', + 'publishing' => 'Publicando...', + 'failed' => 'Fallido', + ], 'empty_state' => [ 'title' => 'No hay plataformas seleccionadas', diff --git a/lang/es/settings.php b/lang/es/settings.php index 08f6fa28..2c733e4b 100644 --- a/lang/es/settings.php +++ b/lang/es/settings.php @@ -75,12 +75,11 @@ 'logo_heading' => 'Logo del workspace', 'logo_description' => 'Sube un logo para tu workspace', 'heading' => 'Nombre del workspace', - 'description' => 'Actualiza el nombre y zona horaria del workspace', + 'description' => 'Actualiza el nombre del workspace', 'members_heading' => 'Miembros', 'members_description' => 'Administra miembros e invitaciones del workspace', 'name' => 'Nombre', 'name_placeholder' => 'Mi Workspace', - 'timezone' => 'Zona horaria', 'save' => 'Guardar', ], diff --git a/lang/es/sidebar.php b/lang/es/sidebar.php index 3ded0919..e4a6141c 100644 --- a/lang/es/sidebar.php +++ b/lang/es/sidebar.php @@ -40,6 +40,7 @@ 'connections' => 'Conexiones', 'hashtags' => 'Hashtags', 'labels' => 'Etiquetas', + 'assets' => 'Medios', 'api_keys' => 'API Keys', 'settings' => 'Configuración', ], diff --git a/lang/es/workspaces.php b/lang/es/workspaces.php index f1d7ac75..4b31446d 100644 --- a/lang/es/workspaces.php +++ b/lang/es/workspaces.php @@ -11,10 +11,42 @@ 'posts' => ':count posts', 'create' => [ - 'title' => 'Crear un workspace', - 'description' => 'Dale un nombre a tu workspace para comenzar', + 'page_title' => 'Crea tu workspace', + 'title' => 'Configura tu workspace', + 'description' => 'Cuéntanos sobre tu marca. Lo usaremos para personalizar las publicaciones generadas por IA con tu voz.', + 'website' => 'Sitio web', + 'website_placeholder' => 'https://tumarca.com', + 'autofill' => 'Autocompletar desde el sitio', + 'autofill_missing_url' => 'Ingresa una URL primero.', + 'autofill_success' => 'Información de la marca cargada.', + 'autofill_error' => 'No se pudo autocompletar. Puedes llenar los campos manualmente.', + 'autofill_errors' => [ + 'unreachable' => 'No pudimos acceder a ese sitio web (:reason).', + 'http_status' => 'El sitio web devolvió un estado inesperado (:status).', + 'invalid_scheme' => 'Solo se admiten URLs http y https.', + 'missing_host' => 'A la URL le falta un host.', + 'unresolvable_host' => 'No pudimos resolver el host (:host).', + 'private_network' => 'No se permiten URLs que apunten a redes privadas.', + ], + 'logo_captured' => 'Logo capturado de tu sitio.', 'name' => 'Nombre del workspace', - 'name_placeholder' => 'Mi Workspace', + 'name_placeholder' => 'ej. Acme Inc', + 'brand_description' => 'Descripción de la marca', + 'brand_description_placeholder' => '¿Qué hace tu marca?', + 'tone' => 'Tono de la marca', + 'tone_professional' => 'Profesional', + 'tone_casual' => 'Casual', + 'tone_friendly' => 'Amigable', + 'tone_bold' => 'Audaz', + 'tone_inspirational' => 'Inspirador', + 'tone_humorous' => 'Humorístico', + 'tone_educational' => 'Educativo', + 'content_language' => 'Idioma del contenido', + 'content_language_description' => 'Las descripciones generadas por IA se escribirán en este idioma.', + 'voice_notes' => 'Notas de voz (opcional)', + 'voice_notes_placeholder' => 'ej. frases cortas y directas. evita jerga.', 'submit' => 'Crear workspace', + 'first_workspace_success' => 'Workspace creado. Conecta una cuenta social para empezar a publicar.', + 'success' => 'Workspace creado.', ], ]; diff --git a/lang/php_en.json b/lang/php_en.json new file mode 100644 index 00000000..f05ab09c --- /dev/null +++ b/lang/php_en.json @@ -0,0 +1 @@ +{"auth.failed":"These credentials do not match our records.","auth.password":"The provided password is incorrect.","auth.throttle":"Too many login attempts. Please try again in :seconds seconds.","pagination.previous":"« Previous","pagination.next":"Next »","passwords.reset":"Your password has been reset.","passwords.sent":"We have emailed your password reset link.","passwords.throttled":"Please wait before retrying.","passwords.token":"This password reset token is invalid.","passwords.user":"We can't find a user with that email address.","validation.accepted":"The :attribute field must be accepted.","validation.accepted_if":"The :attribute field must be accepted when :other is :value.","validation.active_url":"The :attribute field must be a valid URL.","validation.after":"The :attribute field must be a date after :date.","validation.after_or_equal":"The :attribute field must be a date after or equal to :date.","validation.alpha":"The :attribute field must only contain letters.","validation.alpha_dash":"The :attribute field must only contain letters, numbers, dashes, and underscores.","validation.alpha_num":"The :attribute field must only contain letters and numbers.","validation.any_of":"The :attribute field is invalid.","validation.array":"The :attribute field must be an array.","validation.ascii":"The :attribute field must only contain single-byte alphanumeric characters and symbols.","validation.before":"The :attribute field must be a date before :date.","validation.before_or_equal":"The :attribute field must be a date before or equal to :date.","validation.between.array":"The :attribute field must have between :min and :max items.","validation.between.file":"The :attribute field must be between :min and :max kilobytes.","validation.between.numeric":"The :attribute field must be between :min and :max.","validation.between.string":"The :attribute field must be between :min and :max characters.","validation.boolean":"The :attribute field must be true or false.","validation.can":"The :attribute field contains an unauthorized value.","validation.confirmed":"The :attribute field confirmation does not match.","validation.contains":"The :attribute field is missing a required value.","validation.current_password":"The password is incorrect.","validation.date":"The :attribute field must be a valid date.","validation.date_equals":"The :attribute field must be a date equal to :date.","validation.date_format":"The :attribute field must match the format :format.","validation.decimal":"The :attribute field must have :decimal decimal places.","validation.declined":"The :attribute field must be declined.","validation.declined_if":"The :attribute field must be declined when :other is :value.","validation.different":"The :attribute field and :other must be different.","validation.digits":"The :attribute field must be :digits digits.","validation.digits_between":"The :attribute field must be between :min and :max digits.","validation.dimensions":"The :attribute field has invalid image dimensions.","validation.distinct":"The :attribute field has a duplicate value.","validation.doesnt_contain":"The :attribute field must not contain any of the following: :values.","validation.doesnt_end_with":"The :attribute field must not end with one of the following: :values.","validation.doesnt_start_with":"The :attribute field must not start with one of the following: :values.","validation.email":"The :attribute field must be a valid email address.","validation.encoding":"The :attribute field must be encoded in :encoding.","validation.ends_with":"The :attribute field must end with one of the following: :values.","validation.enum":"The selected :attribute is invalid.","validation.exists":"The selected :attribute is invalid.","validation.extensions":"The :attribute field must have one of the following extensions: :values.","validation.file":"The :attribute field must be a file.","validation.filled":"The :attribute field must have a value.","validation.gt.array":"The :attribute field must have more than :value items.","validation.gt.file":"The :attribute field must be greater than :value kilobytes.","validation.gt.numeric":"The :attribute field must be greater than :value.","validation.gt.string":"The :attribute field must be greater than :value characters.","validation.gte.array":"The :attribute field must have :value items or more.","validation.gte.file":"The :attribute field must be greater than or equal to :value kilobytes.","validation.gte.numeric":"The :attribute field must be greater than or equal to :value.","validation.gte.string":"The :attribute field must be greater than or equal to :value characters.","validation.hex_color":"The :attribute field must be a valid hexadecimal color.","validation.image":"The :attribute field must be an image.","validation.in":"The selected :attribute is invalid.","validation.in_array":"The :attribute field must exist in :other.","validation.in_array_keys":"The :attribute field must contain at least one of the following keys: :values.","validation.integer":"The :attribute field must be an integer.","validation.ip":"The :attribute field must be a valid IP address.","validation.ipv4":"The :attribute field must be a valid IPv4 address.","validation.ipv6":"The :attribute field must be a valid IPv6 address.","validation.json":"The :attribute field must be a valid JSON string.","validation.list":"The :attribute field must be a list.","validation.lowercase":"The :attribute field must be lowercase.","validation.lt.array":"The :attribute field must have less than :value items.","validation.lt.file":"The :attribute field must be less than :value kilobytes.","validation.lt.numeric":"The :attribute field must be less than :value.","validation.lt.string":"The :attribute field must be less than :value characters.","validation.lte.array":"The :attribute field must not have more than :value items.","validation.lte.file":"The :attribute field must be less than or equal to :value kilobytes.","validation.lte.numeric":"The :attribute field must be less than or equal to :value.","validation.lte.string":"The :attribute field must be less than or equal to :value characters.","validation.mac_address":"The :attribute field must be a valid MAC address.","validation.max.array":"The :attribute field must not have more than :max items.","validation.max.file":"The :attribute field must not be greater than :max kilobytes.","validation.max.numeric":"The :attribute field must not be greater than :max.","validation.max.string":"The :attribute field must not be greater than :max characters.","validation.max_digits":"The :attribute field must not have more than :max digits.","validation.mimes":"The :attribute field must be a file of type: :values.","validation.mimetypes":"The :attribute field must be a file of type: :values.","validation.min.array":"The :attribute field must have at least :min items.","validation.min.file":"The :attribute field must be at least :min kilobytes.","validation.min.numeric":"The :attribute field must be at least :min.","validation.min.string":"The :attribute field must be at least :min characters.","validation.min_digits":"The :attribute field must have at least :min digits.","validation.missing":"The :attribute field must be missing.","validation.missing_if":"The :attribute field must be missing when :other is :value.","validation.missing_unless":"The :attribute field must be missing unless :other is :value.","validation.missing_with":"The :attribute field must be missing when :values is present.","validation.missing_with_all":"The :attribute field must be missing when :values are present.","validation.multiple_of":"The :attribute field must be a multiple of :value.","validation.not_in":"The selected :attribute is invalid.","validation.not_regex":"The :attribute field format is invalid.","validation.numeric":"The :attribute field must be a number.","validation.password.letters":"The :attribute field must contain at least one letter.","validation.password.mixed":"The :attribute field must contain at least one uppercase and one lowercase letter.","validation.password.numbers":"The :attribute field must contain at least one number.","validation.password.symbols":"The :attribute field must contain at least one symbol.","validation.password.uncompromised":"The given :attribute has appeared in a data leak. Please choose a different :attribute.","validation.present":"The :attribute field must be present.","validation.present_if":"The :attribute field must be present when :other is :value.","validation.present_unless":"The :attribute field must be present unless :other is :value.","validation.present_with":"The :attribute field must be present when :values is present.","validation.present_with_all":"The :attribute field must be present when :values are present.","validation.prohibited":"The :attribute field is prohibited.","validation.prohibited_if":"The :attribute field is prohibited when :other is :value.","validation.prohibited_if_accepted":"The :attribute field is prohibited when :other is accepted.","validation.prohibited_if_declined":"The :attribute field is prohibited when :other is declined.","validation.prohibited_unless":"The :attribute field is prohibited unless :other is in :values.","validation.prohibits":"The :attribute field prohibits :other from being present.","validation.regex":"The :attribute field format is invalid.","validation.required":"The :attribute field is required.","validation.required_array_keys":"The :attribute field must contain entries for: :values.","validation.required_if":"The :attribute field is required when :other is :value.","validation.required_if_accepted":"The :attribute field is required when :other is accepted.","validation.required_if_declined":"The :attribute field is required when :other is declined.","validation.required_unless":"The :attribute field is required unless :other is in :values.","validation.required_with":"The :attribute field is required when :values is present.","validation.required_with_all":"The :attribute field is required when :values are present.","validation.required_without":"The :attribute field is required when :values is not present.","validation.required_without_all":"The :attribute field is required when none of :values are present.","validation.same":"The :attribute field must match :other.","validation.size.array":"The :attribute field must contain :size items.","validation.size.file":"The :attribute field must be :size kilobytes.","validation.size.numeric":"The :attribute field must be :size.","validation.size.string":"The :attribute field must be :size characters.","validation.starts_with":"The :attribute field must start with one of the following: :values.","validation.string":"The :attribute field must be a string.","validation.timezone":"The :attribute field must be a valid timezone.","validation.unique":"The :attribute has already been taken.","validation.uploaded":"The :attribute failed to upload.","validation.uppercase":"The :attribute field must be uppercase.","validation.url":"The :attribute field must be a valid URL.","validation.ulid":"The :attribute field must be a valid ULID.","validation.uuid":"The :attribute field must be a valid UUID.","validation.custom.attribute-name.rule-name":"custom-message","accounts.title":"Connections","accounts.page_title":"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 or video...","assistant.thinking":"Thinking...","assistant.add_to_post":"Add to post","assistant.added":"Added","assistant.error":"Something went wrong. Please try again.","assistant.retry":"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, 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.","assistant.content_blocked":"I can't help with that type of content. I'm here to help you create safe, engaging social media content.","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","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.time":"Time","posts.edit.cancel":"Cancel","posts.edit.delete":"Delete","posts.edit.schedule_for":"Schedule for","posts.edit.scheduled_for":"Scheduled for","posts.edit.schedule_date":"Schedule date","posts.edit.saving":"Saving...","posts.edit.saved":"Saved","posts.edit.draft":"Draft","posts.edit.media":"Media","posts.edit.add_media":"Add media","posts.edit.caption":"Caption","posts.edit.caption_placeholder":"Write your caption...","posts.edit.compose_title":"Create a post","posts.edit.compose_subtitle":"Compose your message and add media","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","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.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.brand.content_language":"Content language","settings.brand.content_language_description":"Language used for AI-generated captions, hashtags, and any text inside generated images or videos.","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.page_title":"Create your workspace","workspaces.create.title":"Set up your workspace","workspaces.create.description":"Tell us about your brand. We'll use this to tailor AI-generated posts to your voice.","workspaces.create.website":"Website","workspaces.create.website_placeholder":"https://yourbrand.com","workspaces.create.autofill":"Autofill from website","workspaces.create.autofill_missing_url":"Enter a URL first.","workspaces.create.autofill_success":"Brand info loaded.","workspaces.create.autofill_error":"Could not autofill. You can fill the fields manually.","workspaces.create.autofill_errors.unreachable":"We could not reach that website (:reason).","workspaces.create.autofill_errors.http_status":"The website returned an unexpected status (:status).","workspaces.create.autofill_errors.invalid_scheme":"Only http and https URLs are supported.","workspaces.create.autofill_errors.missing_host":"The URL is missing a host.","workspaces.create.autofill_errors.unresolvable_host":"We could not resolve the host (:host).","workspaces.create.autofill_errors.private_network":"URLs pointing to private networks are not allowed.","workspaces.create.logo_captured":"Logo captured from your website.","workspaces.create.name":"Workspace name","workspaces.create.name_placeholder":"e.g. Acme Inc","workspaces.create.brand_description":"Brand description","workspaces.create.brand_description_placeholder":"What does your brand do?","workspaces.create.tone":"Brand tone","workspaces.create.tone_professional":"Professional","workspaces.create.tone_casual":"Casual","workspaces.create.tone_friendly":"Friendly","workspaces.create.tone_bold":"Bold","workspaces.create.tone_inspirational":"Inspirational","workspaces.create.tone_humorous":"Humorous","workspaces.create.tone_educational":"Educational","workspaces.create.content_language":"Content language","workspaces.create.content_language_description":"AI-generated captions will be written in this language.","workspaces.create.voice_notes":"Voice notes (optional)","workspaces.create.voice_notes_placeholder":"e.g. short, punchy sentences. avoid jargon.","workspaces.create.submit":"Create workspace","workspaces.create.first_workspace_success":"Workspace created. Connect a social account to start posting.","workspaces.create.success":"Workspace created."} \ No newline at end of file diff --git a/lang/php_es.json b/lang/php_es.json new file mode 100644 index 00000000..cac55fe8 --- /dev/null +++ b/lang/php_es.json @@ -0,0 +1 @@ +{"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.","assets.title":"Medios","assets.tabs.my_uploads":"Mis subidas","assets.tabs.stock_photos":"Fotos gratuitas","assets.tabs.gifs":"GIFs","assets.upload.drag_drop":"Arrastra y suelta tus archivos aquí o haz clic para seleccionar","assets.upload.formats":"JPEG, PNG, GIF, WebP, MP4","assets.upload.uploading":"Subiendo...","assets.empty.title":"Todavía no hay medios","assets.empty.description":"Sube imágenes y videos para construir tu biblioteca de medios.","assets.save_to_assets":"Guardar en la biblioteca","assets.saved":"¡Guardado en tu biblioteca!","assets.create_post":"Crear post","assets.delete.title":"Eliminar medio","assets.delete.description":"¿Estás seguro de que deseas eliminar este medio? Esta acción no se puede deshacer.","assets.delete.confirm":"Eliminar","assets.delete.cancel":"Cancelar","assets.unsplash.search_placeholder":"Buscar fotos gratuitas...","assets.unsplash.no_results":"No se encontraron fotos","assets.unsplash.no_results_description":"Prueba con otro término de búsqueda.","assets.unsplash.trending":"Tendencias en Unsplash","assets.unsplash.start_searching":"Busca fotos gratuitas de Unsplash","assets.giphy.trending":"Tendencias en Giphy","assets.giphy.search_placeholder":"Buscar GIFs...","assets.giphy.no_results":"No se encontraron GIFs","assets.giphy.no_results_description":"Prueba con otro término de búsqueda.","assets.giphy.powered_by":"Powered by GIPHY","assistant.placeholder":"Pídeme escribir una descripción, generar una imagen o video...","assistant.thinking":"Pensando...","assistant.add_to_post":"Añadir al post","assistant.added":"Añadido","assistant.error":"Algo salió mal. Inténtalo de nuevo.","assistant.retry":"Intentar de nuevo","assistant.image_generated":"Aquí está la imagen generada:","assistant.video_generated":"Aquí está el video generado:","assistant.audio_generated":"Aquí está el audio generado:","assistant.empty":"Pregúntame lo que quieras. Puedo escribir descripciones, generar imágenes y producir videos.","assistant.limit_reached_images":"Has alcanzado el límite mensual de generación de imágenes.","assistant.limit_reached_videos":"Has alcanzado el límite mensual de generación de videos.","assistant.content_blocked":"No puedo ayudar con ese tipo de contenido. Estoy aquí para ayudarte a crear contenido seguro y atractivo para redes sociales.","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","comments.placeholder":"Escribe un comentario...","comments.reply_placeholder":"Escribe una respuesta...","comments.reply":"Responder","comments.edit":"Editar","comments.delete":"Eliminar","comments.edited":"editado","comments.save":"Guardar","comments.cancel":"Cancelar","comments.send":"Enviar","comments.replying_to":"Respondiendo a :name","comments.empty":"Todavía no hay comentarios. Inicia la conversación.","comments.load_more":"Cargar comentarios anteriores","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","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.scheduled_for":"Programado para","posts.edit.saving":"Guardando...","posts.edit.saved":"Guardado","posts.edit.draft":"Borrador","posts.edit.scheduled_at":"Programado:","posts.edit.published_at":"Publicado:","posts.edit.media":"Multimedia","posts.edit.add_media":"Añadir media","posts.edit.caption":"Descripción","posts.edit.caption_placeholder":"Escribe tu descripción...","posts.edit.compose_title":"Crear un post","posts.edit.compose_subtitle":"Compón tu mensaje y agrega media","posts.edit.drag_drop":"Arrastra y suelta o haz clic para subir","posts.edit.publish_to":"Publicar en","posts.edit.organize":"Organizar","posts.edit.no_caption":"Sin descripción","posts.edit.no_content":"Sin contenido","posts.edit.no_labels":"Todavía no hay etiquetas creadas","posts.edit.pick_time":"Elegir hora","posts.edit.post_now":"Publicar ahora","posts.edit.time":"Hora","posts.edit.cancel":"Cancelar","posts.edit.schedule_date":"Fecha de programación","posts.edit.view_on_platform":"Ver en la plataforma","posts.edit.platform_status":"Estado de la plataforma","posts.edit.tabs.preview":"Vista previa","posts.edit.tabs.schedule":"Programación","posts.edit.tabs.comments":"Comentarios","posts.edit.tabs.comments_empty":"Todavía no hay comentarios.","posts.edit.tabs.writing_assistant":"Asistente IA","posts.edit.tabs.writing_assistant_empty":"Asistente de escritura próximamente.","posts.edit.status.published":"Publicado","posts.edit.status.publishing":"Publicando...","posts.edit.status.failed":"Fallido","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 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.save":"Guardar","settings.brand.title":"Marca","settings.brand.description":"Configura la identidad de tu marca para el contenido generado por IA.","settings.brand.website":"Sitio web","settings.brand.website_placeholder":"https://tumarca.com","settings.brand.brand_description":"Descripción","settings.brand.brand_description_placeholder":"Cuéntanos sobre tu marca, lo que haces y quién es tu audiencia...","settings.brand.tone":"Tono de voz","settings.brand.tone_professional":"Profesional","settings.brand.tone_casual":"Casual","settings.brand.tone_friendly":"Amigable","settings.brand.tone_bold":"Audaz","settings.brand.tone_inspirational":"Inspirador","settings.brand.tone_humorous":"Humorístico","settings.brand.tone_educational":"Educativo","settings.brand.voice_notes":"Notas de voz","settings.brand.voice_notes_placeholder":"Directrices adicionales de escritura, palabras a evitar, preferencias de estilo...","settings.brand.content_language":"Idioma del contenido","settings.brand.content_language_description":"Idioma usado en los subtítulos, hashtags y cualquier texto dentro de imágenes o videos generados por IA.","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.assets":"Medios","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.page_title":"Crea tu workspace","workspaces.create.title":"Configura tu workspace","workspaces.create.description":"Cuéntanos sobre tu marca. Lo usaremos para personalizar las publicaciones generadas por IA con tu voz.","workspaces.create.website":"Sitio web","workspaces.create.website_placeholder":"https://tumarca.com","workspaces.create.autofill":"Autocompletar desde el sitio","workspaces.create.autofill_missing_url":"Ingresa una URL primero.","workspaces.create.autofill_success":"Información de la marca cargada.","workspaces.create.autofill_error":"No se pudo autocompletar. Puedes llenar los campos manualmente.","workspaces.create.autofill_errors.unreachable":"No pudimos acceder a ese sitio web (:reason).","workspaces.create.autofill_errors.http_status":"El sitio web devolvió un estado inesperado (:status).","workspaces.create.autofill_errors.invalid_scheme":"Solo se admiten URLs http y https.","workspaces.create.autofill_errors.missing_host":"A la URL le falta un host.","workspaces.create.autofill_errors.unresolvable_host":"No pudimos resolver el host (:host).","workspaces.create.autofill_errors.private_network":"No se permiten URLs que apunten a redes privadas.","workspaces.create.logo_captured":"Logo capturado de tu sitio.","workspaces.create.name":"Nombre del workspace","workspaces.create.name_placeholder":"ej. Acme Inc","workspaces.create.brand_description":"Descripción de la marca","workspaces.create.brand_description_placeholder":"¿Qué hace tu marca?","workspaces.create.tone":"Tono de la marca","workspaces.create.tone_professional":"Profesional","workspaces.create.tone_casual":"Casual","workspaces.create.tone_friendly":"Amigable","workspaces.create.tone_bold":"Audaz","workspaces.create.tone_inspirational":"Inspirador","workspaces.create.tone_humorous":"Humorístico","workspaces.create.tone_educational":"Educativo","workspaces.create.content_language":"Idioma del contenido","workspaces.create.content_language_description":"Las descripciones generadas por IA se escribirán en este idioma.","workspaces.create.voice_notes":"Notas de voz (opcional)","workspaces.create.voice_notes_placeholder":"ej. frases cortas y directas. evita jerga.","workspaces.create.submit":"Crear workspace","workspaces.create.first_workspace_success":"Workspace creado. Conecta una cuenta social para empezar a publicar.","workspaces.create.success":"Workspace creado."} \ No newline at end of file diff --git a/lang/php_pt-BR.json b/lang/php_pt-BR.json new file mode 100644 index 00000000..ad1823a6 --- /dev/null +++ b/lang/php_pt-BR.json @@ -0,0 +1 @@ +{"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.","assets.title":"Mídias","assets.tabs.my_uploads":"Meus uploads","assets.tabs.stock_photos":"Fotos gratuitas","assets.tabs.gifs":"GIFs","assets.upload.drag_drop":"Arraste e solte seus arquivos aqui ou clique para selecionar","assets.upload.formats":"JPEG, PNG, GIF, WebP, MP4","assets.upload.uploading":"Enviando...","assets.empty.title":"Nenhuma mídia ainda","assets.empty.description":"Envie imagens e vídeos para criar sua biblioteca de mídia.","assets.save_to_assets":"Salvar na biblioteca","assets.saved":"Salvo na sua biblioteca!","assets.create_post":"Criar post","assets.delete.title":"Excluir mídia","assets.delete.description":"Tem certeza que deseja excluir esta mídia? Esta ação não pode ser desfeita.","assets.delete.confirm":"Excluir","assets.delete.cancel":"Cancelar","assets.unsplash.search_placeholder":"Buscar fotos gratuitas...","assets.unsplash.no_results":"Nenhuma foto encontrada","assets.unsplash.no_results_description":"Tente outro termo de busca.","assets.unsplash.trending":"Em alta no Unsplash","assets.unsplash.start_searching":"Busque fotos gratuitas do Unsplash","assets.giphy.trending":"Em alta no Giphy","assets.giphy.search_placeholder":"Buscar GIFs...","assets.giphy.no_results":"Nenhum GIF encontrado","assets.giphy.no_results_description":"Tente outro termo de busca.","assets.giphy.powered_by":"Powered by GIPHY","assistant.placeholder":"Me peça para escrever uma legenda, gerar uma imagem ou vídeo...","assistant.thinking":"Pensando...","assistant.add_to_post":"Adicionar ao post","assistant.added":"Adicionado","assistant.error":"Algo deu errado. Tente novamente.","assistant.retry":"Tentar de novo","assistant.image_generated":"Aqui está a imagem gerada:","assistant.video_generated":"Aqui está o vídeo gerado:","assistant.audio_generated":"Aqui está o áudio gerado:","assistant.empty":"Me pergunte qualquer coisa. Posso escrever legendas, gerar imagens e produzir vídeos.","assistant.limit_reached_images":"Você atingiu o limite mensal de geração de imagens.","assistant.limit_reached_videos":"Você atingiu o limite mensal de geração de vídeos.","assistant.content_blocked":"Não posso ajudar com esse tipo de conteúdo. Estou aqui para ajudar você a criar conteúdo seguro e engajador para redes sociais.","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","comments.placeholder":"Escreva um comentário...","comments.reply_placeholder":"Escreva uma resposta...","comments.reply":"Responder","comments.edit":"Editar","comments.delete":"Excluir","comments.edited":"editado","comments.save":"Salvar","comments.cancel":"Cancelar","comments.send":"Enviar","comments.replying_to":"Respondendo a :name","comments.empty":"Nenhum comentário ainda. Comece a conversa.","comments.load_more":"Carregar comentários antigos","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","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.scheduled_for":"Agendado para","posts.edit.saving":"Salvando...","posts.edit.saved":"Salvo","posts.edit.draft":"Rascunho","posts.edit.scheduled_at":"Agendado:","posts.edit.published_at":"Publicado:","posts.edit.media":"Mídia","posts.edit.add_media":"Adicionar mídia","posts.edit.caption":"Legenda","posts.edit.caption_placeholder":"Escreva sua legenda...","posts.edit.compose_title":"Crie um post","posts.edit.compose_subtitle":"Componha sua mensagem e adicione mídia","posts.edit.drag_drop":"Arraste e solte ou clique para enviar","posts.edit.publish_to":"Publicar em","posts.edit.organize":"Organizar","posts.edit.no_caption":"Sem legenda","posts.edit.no_content":"Sem conteúdo","posts.edit.no_labels":"Nenhuma etiqueta criada ainda","posts.edit.pick_time":"Escolher horário","posts.edit.post_now":"Publicar agora","posts.edit.time":"Horário","posts.edit.cancel":"Cancelar","posts.edit.schedule_date":"Data de agendamento","posts.edit.view_on_platform":"Ver na plataforma","posts.edit.platform_status":"Status da plataforma","posts.edit.tabs.preview":"Pré-visualização","posts.edit.tabs.schedule":"Agendamento","posts.edit.tabs.comments":"Comentários","posts.edit.tabs.comments_empty":"Nenhum comentário ainda.","posts.edit.tabs.writing_assistant":"Assistente IA","posts.edit.tabs.writing_assistant_empty":"Assistente de escrita em breve.","posts.edit.status.published":"Publicado","posts.edit.status.publishing":"Publicando...","posts.edit.status.failed":"Falhou","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 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.save":"Salvar","settings.brand.title":"Marca","settings.brand.description":"Configure a identidade da sua marca para os conteúdos gerados por AI.","settings.brand.website":"Site","settings.brand.website_placeholder":"https://suamarca.com","settings.brand.brand_description":"Descrição","settings.brand.brand_description_placeholder":"Conte sobre sua marca, o que você faz e quem é seu público...","settings.brand.tone":"Tom de voz","settings.brand.tone_professional":"Profissional","settings.brand.tone_casual":"Casual","settings.brand.tone_friendly":"Amigável","settings.brand.tone_bold":"Ousado","settings.brand.tone_inspirational":"Inspirador","settings.brand.tone_humorous":"Bem-humorado","settings.brand.tone_educational":"Educacional","settings.brand.voice_notes":"Notas de voz","settings.brand.voice_notes_placeholder":"Diretrizes adicionais de escrita, palavras a evitar, preferências de estilo...","settings.brand.content_language":"Idioma do conteúdo","settings.brand.content_language_description":"Idioma usado nas legendas, hashtags e em qualquer texto dentro de imagens ou vídeos gerados por AI.","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.assets":"Mídias","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.page_title":"Crie seu workspace","workspaces.create.title":"Configure seu workspace","workspaces.create.description":"Conte sobre sua marca. Vamos usar isso para personalizar posts gerados por IA com a sua voz.","workspaces.create.website":"Site","workspaces.create.website_placeholder":"https://suamarca.com","workspaces.create.autofill":"Preencher do site","workspaces.create.autofill_missing_url":"Informe uma URL primeiro.","workspaces.create.autofill_success":"Informações da marca carregadas.","workspaces.create.autofill_error":"Não foi possível preencher automaticamente. Você pode preencher os campos manualmente.","workspaces.create.autofill_errors.unreachable":"Não conseguimos acessar esse site (:reason).","workspaces.create.autofill_errors.http_status":"O site retornou um status inesperado (:status).","workspaces.create.autofill_errors.invalid_scheme":"Apenas URLs http e https são suportadas.","workspaces.create.autofill_errors.missing_host":"A URL está sem um host.","workspaces.create.autofill_errors.unresolvable_host":"Não conseguimos resolver o host (:host).","workspaces.create.autofill_errors.private_network":"URLs apontando para redes privadas não são permitidas.","workspaces.create.logo_captured":"Logo capturada do seu site.","workspaces.create.name":"Nome do workspace","workspaces.create.name_placeholder":"ex. Acme Inc","workspaces.create.brand_description":"Descrição da marca","workspaces.create.brand_description_placeholder":"O que sua marca faz?","workspaces.create.tone":"Tom da marca","workspaces.create.tone_professional":"Profissional","workspaces.create.tone_casual":"Casual","workspaces.create.tone_friendly":"Amigável","workspaces.create.tone_bold":"Ousado","workspaces.create.tone_inspirational":"Inspirador","workspaces.create.tone_humorous":"Bem-humorado","workspaces.create.tone_educational":"Educacional","workspaces.create.content_language":"Idioma do conteúdo","workspaces.create.content_language_description":"Legendas geradas por IA serão escritas neste idioma.","workspaces.create.voice_notes":"Notas de voz (opcional)","workspaces.create.voice_notes_placeholder":"ex. frases curtas e diretas. sem jargão.","workspaces.create.submit":"Criar workspace","workspaces.create.first_workspace_success":"Workspace criado. Conecte uma conta social para começar a postar.","workspaces.create.success":"Workspace criado."} \ No newline at end of file diff --git a/lang/pt-BR/assets.php b/lang/pt-BR/assets.php new file mode 100644 index 00000000..bdfe709b --- /dev/null +++ b/lang/pt-BR/assets.php @@ -0,0 +1,51 @@ + 'Mídias', + + 'tabs' => [ + 'my_uploads' => 'Meus uploads', + 'stock_photos' => 'Fotos gratuitas', + 'gifs' => 'GIFs', + ], + + 'upload' => [ + 'drag_drop' => 'Arraste e solte seus arquivos aqui ou clique para selecionar', + 'formats' => 'JPEG, PNG, GIF, WebP, MP4', + 'uploading' => 'Enviando...', + ], + + 'empty' => [ + 'title' => 'Nenhuma mídia ainda', + 'description' => 'Envie imagens e vídeos para criar sua biblioteca de mídia.', + ], + + 'save_to_assets' => 'Salvar na biblioteca', + 'saved' => 'Salvo na sua biblioteca!', + 'create_post' => 'Criar post', + + 'delete' => [ + 'title' => 'Excluir mídia', + 'description' => 'Tem certeza que deseja excluir esta mídia? Esta ação não pode ser desfeita.', + 'confirm' => 'Excluir', + 'cancel' => 'Cancelar', + ], + + 'unsplash' => [ + 'search_placeholder' => 'Buscar fotos gratuitas...', + 'no_results' => 'Nenhuma foto encontrada', + 'no_results_description' => 'Tente outro termo de busca.', + 'trending' => 'Em alta no Unsplash', + 'start_searching' => 'Busque fotos gratuitas do Unsplash', + ], + + 'giphy' => [ + 'trending' => 'Em alta no Giphy', + 'search_placeholder' => 'Buscar GIFs...', + 'no_results' => 'Nenhum GIF encontrado', + 'no_results_description' => 'Tente outro termo de busca.', + 'powered_by' => 'Powered by GIPHY', + ], +]; diff --git a/lang/pt-BR/assistant.php b/lang/pt-BR/assistant.php new file mode 100644 index 00000000..7c6f4dba --- /dev/null +++ b/lang/pt-BR/assistant.php @@ -0,0 +1,19 @@ + 'Me peça para escrever uma legenda, gerar uma imagem ou vídeo...', + 'thinking' => 'Pensando...', + 'add_to_post' => 'Adicionar ao post', + 'added' => 'Adicionado', + 'error' => 'Algo deu errado. Tente novamente.', + 'retry' => 'Tentar de novo', + 'image_generated' => 'Aqui está a imagem gerada:', + 'video_generated' => 'Aqui está o vídeo gerado:', + 'audio_generated' => 'Aqui está o áudio gerado:', + 'empty' => 'Me pergunte qualquer coisa. Posso escrever legendas, gerar imagens e produzir vídeos.', + 'limit_reached_images' => 'Você atingiu o limite mensal de geração de imagens.', + 'limit_reached_videos' => 'Você atingiu o limite mensal de geração de vídeos.', + 'content_blocked' => 'Não posso ajudar com esse tipo de conteúdo. Estou aqui para ajudar você a criar conteúdo seguro e engajador para redes sociais.', +]; diff --git a/lang/pt-BR/comments.php b/lang/pt-BR/comments.php new file mode 100644 index 00000000..3df44509 --- /dev/null +++ b/lang/pt-BR/comments.php @@ -0,0 +1,18 @@ + 'Escreva um comentário...', + 'reply_placeholder' => 'Escreva uma resposta...', + 'reply' => 'Responder', + 'edit' => 'Editar', + 'delete' => 'Excluir', + 'edited' => 'editado', + 'save' => 'Salvar', + 'cancel' => 'Cancelar', + 'send' => 'Enviar', + 'replying_to' => 'Respondendo a :name', + 'empty' => 'Nenhum comentário ainda. Comece a conversa.', + 'load_more' => 'Carregar comentários antigos', +]; diff --git a/lang/pt-BR/onboarding.php b/lang/pt-BR/onboarding.php deleted file mode 100644 index 2199deca..00000000 --- a/lang/pt-BR/onboarding.php +++ /dev/null @@ -1,78 +0,0 @@ - [ - 'page_title' => 'Bem-vindo - Conte-nos sobre você', - 'title' => 'Conte-nos sobre você', - 'description' => 'Nos ajude a personalizar sua experiência', - 'submit' => 'Continuar', - ], - - 'personas' => [ - 'founder' => [ - 'label' => 'Fundador', - 'description' => 'Construindo uma startup ou novo negócio', - ], - 'creator' => [ - 'label' => 'Criador de Conteúdo', - 'description' => 'Criador de conteúdo ou influenciador', - ], - 'agency' => [ - 'label' => 'Agência', - 'description' => 'Agência de marketing ou mídias sociais', - ], - 'enterprise' => [ - 'label' => 'Empresa', - 'description' => 'Grande empresa ou corporação', - ], - 'small_business' => [ - 'label' => 'Pequeno Negócio', - 'description' => 'Pequena ou média empresa', - ], - 'personal' => [ - 'label' => 'Pessoal', - 'description' => 'Marca pessoal ou hobby', - ], - ], - - 'brand' => [ - 'page_title' => 'Conte sobre sua marca', - 'title' => 'Conte sobre sua marca', - 'description' => 'Esses valores padrão serão usados em todos os posts gerados por AI neste workspace. Você pode alterar depois em Configurações.', - 'submit' => 'Continuar', - 'skip' => 'Pular por enquanto', - 'autofill' => 'Preencher', - 'autofill_success' => 'Puxamos o que conseguimos do seu site. Revise e ajuste se precisar.', - 'autofill_error' => 'Não conseguimos ler esse site. Preencha manualmente.', - 'autofill_missing_url' => 'Informe a URL do seu site primeiro.', - 'logo_captured' => 'Logo capturado do seu site.', - - 'website' => 'Site', - 'website_placeholder' => 'https://suamarca.com', - 'brand_description' => 'Descrição', - 'brand_description_placeholder' => 'Conte sobre sua marca, o que você faz e quem é seu público...', - 'tone' => 'Tom de voz', - 'tone_professional' => 'Profissional', - 'tone_casual' => 'Casual', - 'tone_friendly' => 'Amigável', - 'tone_bold' => 'Ousado', - 'tone_inspirational' => 'Inspirador', - 'tone_humorous' => 'Bem-humorado', - 'tone_educational' => 'Educacional', - 'voice_notes' => 'Notas de voz', - 'voice_notes_placeholder' => 'Diretrizes adicionais de escrita, palavras a evitar, preferências de estilo...', - 'content_language' => 'Idioma do conteúdo', - 'content_language_description' => 'Idioma usado nas legendas, hashtags e em qualquer texto dentro de imagens ou vídeos gerados por AI.', - ], - - 'connect' => [ - 'page_title' => 'Conecte suas contas', - 'title' => 'Conecte suas contas', - 'description' => 'Conecte pelo menos uma rede social para começar', - 'submit' => 'Continuar', - 'error' => 'Algo deu errado. Tente novamente.', - 'go_back' => 'Voltar', - ], -]; diff --git a/lang/pt-BR/posts.php b/lang/pt-BR/posts.php index 63fdc04c..53267ac8 100644 --- a/lang/pt-BR/posts.php +++ b/lang/pt-BR/posts.php @@ -87,14 +87,46 @@ 'delete' => 'Excluir', 'settings' => 'Configurações', 'schedule_for' => 'Agendar para', + 'scheduled_for' => 'Agendado para', 'saving' => 'Salvando...', 'saved' => 'Salvo', + 'draft' => 'Rascunho', 'scheduled_at' => 'Agendado:', 'published_at' => 'Publicado:', 'media' => 'Mídia', + 'add_media' => 'Adicionar mídia', 'caption' => 'Legenda', + 'caption_placeholder' => 'Escreva sua legenda...', + 'compose_title' => 'Crie um post', + 'compose_subtitle' => 'Componha sua mensagem e adicione mídia', + 'drag_drop' => 'Arraste e solte ou clique para enviar', + 'publish_to' => 'Publicar em', + 'organize' => 'Organizar', 'no_caption' => 'Sem legenda', 'no_content' => 'Sem conteúdo', + 'no_labels' => 'Nenhuma etiqueta criada ainda', + 'pick_time' => 'Escolher horário', + 'post_now' => 'Publicar agora', + 'time' => 'Horário', + 'cancel' => 'Cancelar', + 'schedule_date' => 'Data de agendamento', + 'view_on_platform' => 'Ver na plataforma', + 'platform_status' => 'Status da plataforma', + + 'tabs' => [ + 'preview' => 'Pré-visualização', + 'schedule' => 'Agendamento', + 'comments' => 'Comentários', + 'comments_empty' => 'Nenhum comentário ainda.', + 'writing_assistant' => 'Assistente IA', + 'writing_assistant_empty' => 'Assistente de escrita em breve.', + ], + + 'status' => [ + 'published' => 'Publicado', + 'publishing' => 'Publicando...', + 'failed' => 'Falhou', + ], 'empty_state' => [ 'title' => 'Nenhuma plataforma selecionada', diff --git a/lang/pt-BR/settings.php b/lang/pt-BR/settings.php index bf8a7b99..617984e0 100644 --- a/lang/pt-BR/settings.php +++ b/lang/pt-BR/settings.php @@ -75,12 +75,11 @@ 'logo_heading' => 'Logo do workspace', 'logo_description' => 'Envie um logo para o workspace', 'heading' => 'Nome do workspace', - 'description' => 'Atualize o nome e fuso horário do workspace', + 'description' => 'Atualize o nome do workspace', 'members_heading' => 'Membros', 'members_description' => 'Gerencie membros e convites do workspace', 'name' => 'Nome', 'name_placeholder' => 'Meu Workspace', - 'timezone' => 'Fuso horário', 'save' => 'Salvar', ], diff --git a/lang/pt-BR/sidebar.php b/lang/pt-BR/sidebar.php index e0d7fca2..048f50cd 100644 --- a/lang/pt-BR/sidebar.php +++ b/lang/pt-BR/sidebar.php @@ -40,6 +40,7 @@ 'connections' => 'Conexões', 'hashtags' => 'Hashtags', 'labels' => 'Etiquetas', + 'assets' => 'Mídias', 'api_keys' => 'API Keys', 'settings' => 'Configurações', ], diff --git a/lang/pt-BR/workspaces.php b/lang/pt-BR/workspaces.php index 5f13371d..9dd4b5c0 100644 --- a/lang/pt-BR/workspaces.php +++ b/lang/pt-BR/workspaces.php @@ -11,10 +11,42 @@ 'posts' => ':count posts', 'create' => [ - 'title' => 'Criar um workspace', - 'description' => 'Dê um nome ao seu workspace para começar', + 'page_title' => 'Crie seu workspace', + 'title' => 'Configure seu workspace', + 'description' => 'Conte sobre sua marca. Vamos usar isso para personalizar posts gerados por IA com a sua voz.', + 'website' => 'Site', + 'website_placeholder' => 'https://suamarca.com', + 'autofill' => 'Preencher do site', + 'autofill_missing_url' => 'Informe uma URL primeiro.', + 'autofill_success' => 'Informações da marca carregadas.', + 'autofill_error' => 'Não foi possível preencher automaticamente. Você pode preencher os campos manualmente.', + 'autofill_errors' => [ + 'unreachable' => 'Não conseguimos acessar esse site (:reason).', + 'http_status' => 'O site retornou um status inesperado (:status).', + 'invalid_scheme' => 'Apenas URLs http e https são suportadas.', + 'missing_host' => 'A URL está sem um host.', + 'unresolvable_host' => 'Não conseguimos resolver o host (:host).', + 'private_network' => 'URLs apontando para redes privadas não são permitidas.', + ], + 'logo_captured' => 'Logo capturada do seu site.', 'name' => 'Nome do workspace', - 'name_placeholder' => 'Meu Workspace', + 'name_placeholder' => 'ex. Acme Inc', + 'brand_description' => 'Descrição da marca', + 'brand_description_placeholder' => 'O que sua marca faz?', + 'tone' => 'Tom da marca', + 'tone_professional' => 'Profissional', + 'tone_casual' => 'Casual', + 'tone_friendly' => 'Amigável', + 'tone_bold' => 'Ousado', + 'tone_inspirational' => 'Inspirador', + 'tone_humorous' => 'Bem-humorado', + 'tone_educational' => 'Educacional', + 'content_language' => 'Idioma do conteúdo', + 'content_language_description' => 'Legendas geradas por IA serão escritas neste idioma.', + 'voice_notes' => 'Notas de voz (opcional)', + 'voice_notes_placeholder' => 'ex. frases curtas e diretas. sem jargão.', 'submit' => 'Criar workspace', + 'first_workspace_success' => 'Workspace criado. Conecte uma conta social para começar a postar.', + 'success' => 'Workspace criado.', ], ]; diff --git a/resources/js/components/ImagePreviewDialog.vue b/resources/js/components/ImagePreviewDialog.vue new file mode 100644 index 00000000..50626d98 --- /dev/null +++ b/resources/js/components/ImagePreviewDialog.vue @@ -0,0 +1,33 @@ + + + diff --git a/resources/js/components/TimezoneCombobox.vue b/resources/js/components/TimezoneCombobox.vue deleted file mode 100644 index de3749a8..00000000 --- a/resources/js/components/TimezoneCombobox.vue +++ /dev/null @@ -1,131 +0,0 @@ - - - diff --git a/resources/js/components/posts/PickTimePopover.vue b/resources/js/components/posts/PickTimePopover.vue new file mode 100644 index 00000000..c3e1877c --- /dev/null +++ b/resources/js/components/posts/PickTimePopover.vue @@ -0,0 +1,116 @@ + + + diff --git a/resources/js/components/posts/editor/ScheduleTab.vue b/resources/js/components/posts/editor/ScheduleTab.vue index b06002f8..23e02fb9 100644 --- a/resources/js/components/posts/editor/ScheduleTab.vue +++ b/resources/js/components/posts/editor/ScheduleTab.vue @@ -45,13 +45,23 @@ interface PostPlatform { meta?: Record; } +interface Label { + id: string; + name: string; + color: string; +} + const props = defineProps<{ postPlatforms: PostPlatform[]; selectedPlatformIds: string[]; + labels: Label[]; + selectedLabelIds: string[]; + isReadOnly: boolean; }>(); const emit = defineEmits<{ togglePlatform: [platformId: string]; + toggleLabel: [labelId: string]; }>(); const platformIcons: Record = { @@ -76,20 +86,30 @@ const getPlatformDisplayName = (pp: PostPlatform): string => const getPlatformAvatar = (pp: PostPlatform): string | null => pp.social_account?.avatar_url ?? pp.platform_avatar ?? null; + diff --git a/resources/js/components/posts/editor/WritingAssistantTab.vue b/resources/js/components/posts/editor/WritingAssistantTab.vue index 3c05b0fd..d598f216 100644 --- a/resources/js/components/posts/editor/WritingAssistantTab.vue +++ b/resources/js/components/posts/editor/WritingAssistantTab.vue @@ -1,7 +1,28 @@ diff --git a/resources/js/pages/onboarding/Account.vue b/resources/js/pages/onboarding/Account.vue deleted file mode 100644 index d16c0915..00000000 --- a/resources/js/pages/onboarding/Account.vue +++ /dev/null @@ -1,146 +0,0 @@ - - - diff --git a/resources/js/pages/onboarding/Brand.vue b/resources/js/pages/onboarding/Brand.vue deleted file mode 100644 index 7e460b77..00000000 --- a/resources/js/pages/onboarding/Brand.vue +++ /dev/null @@ -1,246 +0,0 @@ - - -