From 6f96d67dbcc089a27918a0f7102ffb160b94c0d0 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 19 May 2026 08:16:43 -0300 Subject: [PATCH 01/14] fix(social): distinguish platform-down from token-expired MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a provider's API was down (5xx, timeout, DNS), the hourly RefreshSocialToken job and daily VerifyWorkspaceConnections job were treating it as "token revoked" and emailing the user to reconnect. Bluesky going offline triggered false-positive disconnect notifications because Bluesky access tokens are short-lived (2h) so every hourly refresh failed during the outage. - New PlatformUnavailableException: API unreachable / 5xx, transient. TokenExpiredException stays for 4xx (token is provably bad). - New TokenRefreshClient: normalizes failure semantics for OAuth refresh HTTP calls across all providers. Takes a Platform enum so typos fail at compile time and the user-facing label comes from one source. - ConnectionVerifier: all 8 refresh*Token methods route through the new client. Hardcoded OAuth URLs (LinkedIn, YouTube) and Bluesky's default PDS host moved into config/trypost.php alongside the existing per-platform entries. - RefreshSocialToken job: PlatformUnavailableException → log warning and stop. Do NOT markAsTokenExpired, do NOT notify the user. Next scheduled tick retries. - VerifyWorkspaceConnections job: PlatformUnavailableException from the inner refresh propagates and is treated as a transient skip. --- .../PlatformUnavailableException.php | 23 +++ app/Jobs/RefreshSocialToken.php | 11 ++ app/Jobs/VerifyWorkspaceConnections.php | 11 ++ app/Services/Social/ConnectionVerifier.php | 166 ++++++------------ app/Services/Social/TokenRefreshClient.php | 87 +++++++++ config/trypost.php | 5 + tests/Feature/Jobs/RefreshSocialTokenTest.php | 23 +++ .../Social/ConnectionVerifierTest.php | 70 ++++++++ .../VerifyWorkspaceConnectionsTest.php | 21 +++ 9 files changed, 309 insertions(+), 108 deletions(-) create mode 100644 app/Exceptions/PlatformUnavailableException.php create mode 100644 app/Services/Social/TokenRefreshClient.php diff --git a/app/Exceptions/PlatformUnavailableException.php b/app/Exceptions/PlatformUnavailableException.php new file mode 100644 index 00000000..27c10e4b --- /dev/null +++ b/app/Exceptions/PlatformUnavailableException.php @@ -0,0 +1,23 @@ +refreshToken($this->account); + } catch (PlatformUnavailableException $e) { + // Platform is down (5xx / network). Leave the account alone — + // next scheduled run will try again. Critically, do NOT mark + // the account expired: that would trigger a false-positive + // "reconnect your account" notification. + Log::warning('Token refresh skipped: platform unavailable', [ + 'account_id' => $this->account->id, + 'platform' => $this->account->platform->value, + 'error' => $e->getMessage(), + ]); } catch (TokenExpiredException $e) { // refresh_token rejected by the provider (revoked / rotated / // expired beyond refresh). Mark the account so the user is diff --git a/app/Jobs/VerifyWorkspaceConnections.php b/app/Jobs/VerifyWorkspaceConnections.php index d635ed97..5174b13d 100644 --- a/app/Jobs/VerifyWorkspaceConnections.php +++ b/app/Jobs/VerifyWorkspaceConnections.php @@ -7,6 +7,7 @@ use App\Enums\Notification\Channel; use App\Enums\Notification\Type; use App\Enums\SocialAccount\Status; +use App\Exceptions\PlatformUnavailableException; use App\Exceptions\TokenExpiredException; use App\Mail\WorkspaceConnectionsDisconnected; use App\Models\SocialAccount; @@ -63,6 +64,16 @@ private function verifyAccount(ConnectionVerifier $verifier, SocialAccount $acco try { $verifier->verify($account); + return true; + } catch (PlatformUnavailableException $e) { + // Platform is down (5xx / network). The account's token is not + // provably bad — skip silently and try again next pass. + Log::warning('Social account verification skipped: platform unavailable', [ + 'account_id' => $account->id, + 'platform' => $account->platform->value, + 'error' => $e->getMessage(), + ]); + return true; } catch (TokenExpiredException $e) { Log::warning('Social account connection is invalid', [ diff --git a/app/Services/Social/ConnectionVerifier.php b/app/Services/Social/ConnectionVerifier.php index d65a493d..e5c9fc17 100644 --- a/app/Services/Social/ConnectionVerifier.php +++ b/app/Services/Social/ConnectionVerifier.php @@ -5,11 +5,11 @@ namespace App\Services\Social; use App\Enums\SocialAccount\Platform; +use App\Exceptions\PlatformUnavailableException; use App\Exceptions\TokenExpiredException; use App\Models\SocialAccount; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; -use Illuminate\Support\Facades\Log; class ConnectionVerifier { @@ -17,6 +17,7 @@ class ConnectionVerifier * Verify that a social account connection is still valid. * * @throws TokenExpiredException if the connection is invalid + * @throws PlatformUnavailableException if the platform's API is down */ public function verify(SocialAccount $account): bool { @@ -27,6 +28,9 @@ public function verify(SocialAccount $account): bool // LinkedIn, etc.) invalidate the previous refresh_token on each // refresh, so proactive refreshes during races cause false-positive // disconnects even though the access_token still works fine. + // + // PlatformUnavailableException from refresh propagates naturally so + // the caller can distinguish "platform is down" from "token bad". if ($account->is_token_expired) { $this->refreshToken($account); @@ -113,17 +117,13 @@ private function refreshLinkedInToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for LinkedIn account'); } - $response = Http::asForm()->post('https://www.linkedin.com/oauth/v2/accessToken', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - 'client_id' => config('services.linkedin.client_id'), - 'client_secret' => config('services.linkedin.client_secret'), - ]); - - if ($response->failed()) { - Log::error('ConnectionVerifier: LinkedIn token refresh failed', ['body' => $this->redactBody($response->body())]); - throw new TokenExpiredException('Failed to refresh LinkedIn token'); - } + $response = TokenRefreshClient::for(Platform::LinkedIn)->send(fn () => Http::asForm() + ->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ + 'grant_type' => 'refresh_token', + 'refresh_token' => $account->refresh_token, + 'client_id' => config('services.linkedin.client_id'), + 'client_secret' => config('services.linkedin.client_secret'), + ])); $data = $response->json(); @@ -145,17 +145,12 @@ private function refreshXToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for X account'); } - $response = Http::asForm() + $response = TokenRefreshClient::for(Platform::X)->send(fn () => Http::asForm() ->withBasicAuth(config('services.x.client_id'), config('services.x.client_secret')) ->post(config('trypost.platforms.x.api').'/oauth2/token', [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, - ]); - - if ($response->failed()) { - Log::error('ConnectionVerifier: X token refresh failed', ['body' => $this->redactBody($response->body())]); - throw new TokenExpiredException('Failed to refresh X token'); - } + ])); $data = $response->json(); @@ -170,13 +165,16 @@ private function refreshXToken(SocialAccount $account): void private function refreshBlueskyToken(SocialAccount $account): void { - $service = $account->meta['service'] ?? 'https://bsky.social'; + $service = $account->meta['service'] ?? config('trypost.platforms.bluesky.default_service'); + $client = TokenRefreshClient::for(Platform::Bluesky); - // Try refresh token first - $response = Http::withToken($account->refresh_token) - ->post("{$service}/xrpc/com.atproto.server.refreshSession"); + // Try refresh token first. Connection errors / 5xx surface as + // PlatformUnavailableException (Bluesky is down — don't touch the + // account's status). 4xx falls through to the re-auth fallback. + try { + $response = $client->send(fn () => Http::withToken($account->refresh_token) + ->post("{$service}/xrpc/com.atproto.server.refreshSession")); - if ($response->successful()) { $data = $response->json(); $account->update([ 'access_token' => data_get($data, 'accessJwt'), @@ -187,35 +185,29 @@ private function refreshBlueskyToken(SocialAccount $account): void $account->refresh(); return; + } catch (TokenExpiredException) { + // refresh token was rejected (4xx) — fall back to re-auth below } - // If refresh fails, re-authenticate with stored credentials if (isset($account->meta['password'])) { try { - $password = decrypt($account->meta['password']); - $identifier = $account->meta['identifier']; + $reauth = $client->send(fn () => Http::post("{$service}/xrpc/com.atproto.server.createSession", [ + 'identifier' => $account->meta['identifier'], + 'password' => decrypt($account->meta['password']), + ])); - $response = Http::post("{$service}/xrpc/com.atproto.server.createSession", [ - 'identifier' => $identifier, - 'password' => $password, + $data = $reauth->json(); + $account->update([ + 'access_token' => data_get($data, 'accessJwt'), + 'refresh_token' => data_get($data, 'refreshJwt'), + 'token_expires_at' => now()->addHours(2), ]); - if ($response->successful()) { - $data = $response->json(); - $account->update([ - 'access_token' => data_get($data, 'accessJwt'), - 'refresh_token' => data_get($data, 'refreshJwt'), - 'token_expires_at' => now()->addHours(2), - ]); + $account->refresh(); - $account->refresh(); - - return; - } - } catch (\Exception $e) { - Log::error('ConnectionVerifier: Bluesky re-authentication failed', [ - 'error' => $e->getMessage(), - ]); + return; + } catch (TokenExpiredException) { + // re-auth rejected with stored credentials — fall through } } @@ -228,17 +220,13 @@ private function refreshYouTubeToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for YouTube account'); } - $response = Http::asForm()->post('https://oauth2.googleapis.com/token', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - 'client_id' => config('services.google.client_id'), - 'client_secret' => config('services.google.client_secret'), - ]); - - if ($response->failed()) { - Log::error('ConnectionVerifier: YouTube token refresh failed', ['body' => $this->redactBody($response->body())]); - throw new TokenExpiredException('Failed to refresh YouTube token'); - } + $response = TokenRefreshClient::for(Platform::YouTube)->send(fn () => Http::asForm() + ->post(config('trypost.platforms.youtube.oauth_api').'/token', [ + 'grant_type' => 'refresh_token', + 'refresh_token' => $account->refresh_token, + 'client_id' => config('services.google.client_id'), + 'client_secret' => config('services.google.client_secret'), + ])); $data = $response->json(); @@ -256,17 +244,13 @@ private function refreshTikTokToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for TikTok account'); } - $response = Http::asForm()->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - 'client_key' => config('services.tiktok.client_id'), - 'client_secret' => config('services.tiktok.client_secret'), - ]); - - if ($response->failed()) { - Log::error('ConnectionVerifier: TikTok token refresh failed', ['body' => $this->redactBody($response->body())]); - throw new TokenExpiredException('Failed to refresh TikTok token'); - } + $response = TokenRefreshClient::for(Platform::TikTok)->send(fn () => Http::asForm() + ->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [ + 'grant_type' => 'refresh_token', + 'refresh_token' => $account->refresh_token, + 'client_key' => config('services.tiktok.client_id'), + 'client_secret' => config('services.tiktok.client_secret'), + ])); $data = $response->json(); @@ -287,18 +271,13 @@ private function refreshPinterestToken(SocialAccount $account): void $credentials = base64_encode(config('services.pinterest.client_id').':'.config('services.pinterest.client_secret')); - $response = Http::withHeaders([ + $response = TokenRefreshClient::for(Platform::Pinterest)->send(fn () => Http::withHeaders([ 'Authorization' => "Basic {$credentials}", 'Content-Type' => 'application/x-www-form-urlencoded', ])->asForm()->post(config('trypost.platforms.pinterest.api').'/oauth/token', [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, - ]); - - if ($response->failed()) { - Log::error('ConnectionVerifier: Pinterest token refresh failed', ['body' => $this->redactBody($response->body())]); - throw new TokenExpiredException('Failed to refresh Pinterest token'); - } + ])); $data = $response->json(); @@ -314,15 +293,10 @@ private function refreshPinterestToken(SocialAccount $account): void private function refreshThreadsToken(SocialAccount $account): void { // Threads uses long-lived tokens that can be refreshed - $response = Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [ + $response = TokenRefreshClient::for(Platform::Threads)->send(fn () => Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [ 'grant_type' => 'th_refresh_token', 'access_token' => $account->access_token, - ]); - - if ($response->failed()) { - Log::error('ConnectionVerifier: Threads token refresh failed', ['body' => $this->redactBody($response->body())]); - throw new TokenExpiredException('Failed to refresh Threads token'); - } + ])); $data = $response->json(); $newToken = data_get($data, 'access_token'); @@ -338,15 +312,10 @@ private function refreshThreadsToken(SocialAccount $account): void private function refreshInstagramToken(SocialAccount $account): void { - $response = Http::get(config('trypost.platforms.instagram.auth_api').'/refresh_access_token', [ + $response = TokenRefreshClient::for(Platform::Instagram)->send(fn () => Http::get(config('trypost.platforms.instagram.auth_api').'/refresh_access_token', [ 'grant_type' => 'ig_refresh_token', 'access_token' => $account->access_token, - ]); - - if ($response->failed()) { - Log::error('ConnectionVerifier: Instagram token refresh failed', ['body' => $this->redactBody($response->body())]); - throw new TokenExpiredException('Failed to refresh Instagram token'); - } + ])); $data = $response->json(); $newToken = data_get($data, 'access_token'); @@ -524,7 +493,7 @@ private function verifyPinterest(SocialAccount $account): bool private function verifyBluesky(SocialAccount $account): bool { - $service = $account->meta['service'] ?? 'https://bsky.social'; + $service = $account->meta['service'] ?? config('trypost.platforms.bluesky.default_service'); $response = Http::withToken($account->access_token) ->get("{$service}/xrpc/app.bsky.actor.getProfile", [ @@ -554,23 +523,4 @@ private function verifyMastodon(SocialAccount $account): bool return $response->successful(); } - - private function redactBody(string $body): string - { - return preg_replace( - [ - '/access_token=([^&"\s]+)/', - '/"access_token"\s*:\s*"([^"]+)"/', - '/Bearer\s+\S+/', - '/"token"\s*:\s*"([^"]+)"/', - ], - [ - 'access_token=[REDACTED]', - '"access_token":"[REDACTED]"', - 'Bearer [REDACTED]', - '"token":"[REDACTED]"', - ], - $body - ); - } } diff --git a/app/Services/Social/TokenRefreshClient.php b/app/Services/Social/TokenRefreshClient.php new file mode 100644 index 00000000..e1959897 --- /dev/null +++ b/app/Services/Social/TokenRefreshClient.php @@ -0,0 +1,87 @@ +platform->label(); + + try { + $response = $request(); + } catch (ConnectionException $e) { + throw new PlatformUnavailableException("{$name} API unreachable: {$e->getMessage()}"); + } + + if ($response->serverError()) { + throw new PlatformUnavailableException( + "{$name} API returned {$response->status()} during token refresh", + $response->status(), + ); + } + + if ($response->failed()) { + Log::error("TokenRefreshClient: {$name} token refresh failed", [ + 'body' => $this->redactBody($response->body()), + ]); + throw new TokenExpiredException("Failed to refresh {$name} token"); + } + + return $response; + } + + private function redactBody(string $body): string + { + return preg_replace( + [ + '/access_token=([^&"\s]+)/', + '/"access_token"\s*:\s*"([^"]+)"/', + '/Bearer\s+\S+/', + '/"token"\s*:\s*"([^"]+)"/', + ], + [ + 'access_token=[REDACTED]', + '"access_token":"[REDACTED]"', + 'Bearer [REDACTED]', + '"token":"[REDACTED]"', + ], + $body + ); + } +} diff --git a/config/trypost.php b/config/trypost.php index 9ab2e2b9..29fcfb95 100644 --- a/config/trypost.php +++ b/config/trypost.php @@ -63,6 +63,8 @@ 'linkedin' => [ 'enabled' => env('LINKEDIN_ENABLED', true), 'api' => env('LINKEDIN_API', 'https://api.linkedin.com'), + // OAuth host is different from the data API (api.linkedin.com). + 'oauth_api' => env('LINKEDIN_OAUTH_API', 'https://www.linkedin.com'), ], 'linkedin-page' => [ 'enabled' => env('LINKEDIN_PAGE_ENABLED', true), @@ -80,6 +82,7 @@ 'enabled' => env('YOUTUBE_ENABLED', true), 'data_api' => env('YOUTUBE_DATA_API', 'https://www.googleapis.com/youtube/v3'), 'analytics_api' => env('YOUTUBE_ANALYTICS_API', 'https://youtubeanalytics.googleapis.com/v2'), + 'oauth_api' => env('YOUTUBE_OAUTH_API', 'https://oauth2.googleapis.com'), ], 'facebook' => [ 'enabled' => env('FACEBOOK_ENABLED', true), @@ -108,6 +111,8 @@ 'bluesky' => [ 'enabled' => env('BLUESKY_ENABLED', true), 'public_appview' => env('BLUESKY_PUBLIC_APPVIEW', 'https://public.api.bsky.app'), + // Default PDS used when the account has no `meta.service` override. + 'default_service' => env('BLUESKY_DEFAULT_SERVICE', 'https://bsky.social'), ], 'mastodon' => [ 'enabled' => env('MASTODON_ENABLED', true), diff --git a/tests/Feature/Jobs/RefreshSocialTokenTest.php b/tests/Feature/Jobs/RefreshSocialTokenTest.php index 41b956c1..172076a7 100644 --- a/tests/Feature/Jobs/RefreshSocialTokenTest.php +++ b/tests/Feature/Jobs/RefreshSocialTokenTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use App\Enums\SocialAccount\Status; +use App\Exceptions\PlatformUnavailableException; use App\Exceptions\TokenExpiredException; use App\Jobs\RefreshSocialToken; use App\Jobs\SendNotification; @@ -67,3 +68,25 @@ expect($this->account->fresh()->status)->toBe(Status::Connected); }); + +test('refresh job does NOT mark account expired when platform is unavailable', function () { + Queue::fake(); + + Log::shouldReceive('warning')->once()->withArgs(function ($message, $context) { + return $message === 'Token refresh skipped: platform unavailable' + && $context['account_id'] === $this->account->id + && str_contains($context['error'], '503'); + }); + + $verifier = mock(ConnectionVerifier::class); + $verifier->shouldReceive('refreshToken')->once()->andThrow( + new PlatformUnavailableException('X API returned 503 during token refresh', 503) + ); + app()->instance(ConnectionVerifier::class, $verifier); + + (new RefreshSocialToken($this->account))->handle($verifier); + + // Critically: account status stays Connected, no notification dispatched. + expect($this->account->fresh()->status)->toBe(Status::Connected); + Queue::assertNotPushed(SendNotification::class); +}); diff --git a/tests/Feature/Services/Social/ConnectionVerifierTest.php b/tests/Feature/Services/Social/ConnectionVerifierTest.php index cbadf68d..9d025385 100644 --- a/tests/Feature/Services/Social/ConnectionVerifierTest.php +++ b/tests/Feature/Services/Social/ConnectionVerifierTest.php @@ -2,9 +2,11 @@ declare(strict_types=1); +use App\Exceptions\PlatformUnavailableException; use App\Exceptions\TokenExpiredException; use App\Models\SocialAccount; use App\Services\Social\ConnectionVerifier; +use Illuminate\Http\Client\ConnectionException; use Illuminate\Support\Facades\Http; test('verifies account without refresh when token is not expired', function () { @@ -360,3 +362,71 @@ expect(fn () => $verifier->verify($account))->toThrow(TokenExpiredException::class); }); + +test('5xx during refresh raises PlatformUnavailableException, not TokenExpiredException', function () { + Http::fake([ + 'bsky.social/xrpc/com.atproto.server.refreshSession' => Http::response('upstream timeout', 503), + ]); + + $account = SocialAccount::factory()->bluesky()->create([ + 'token_expires_at' => now()->subMinutes(5), + 'refresh_token' => 'old_refresh_token', + 'meta' => ['service' => 'https://bsky.social'], + ]); + + $verifier = new ConnectionVerifier; + + expect(fn () => $verifier->refreshToken($account))->toThrow(PlatformUnavailableException::class); +}); + +test('connection failure during refresh raises PlatformUnavailableException', function () { + Http::fake([ + 'oauth2.googleapis.com/token' => fn () => throw new ConnectionException('cURL error 7: connection refused'), + ]); + + $account = SocialAccount::factory()->youtube()->create([ + 'token_expires_at' => now()->subMinutes(5), + 'refresh_token' => 'old_refresh_token', + ]); + + $verifier = new ConnectionVerifier; + + expect(fn () => $verifier->refreshToken($account))->toThrow(PlatformUnavailableException::class); +}); + +test('4xx during refresh keeps raising TokenExpiredException', function () { + Http::fake([ + 'api.x.com/2/oauth2/token' => Http::response(['error' => 'invalid_grant'], 400), + ]); + + $account = SocialAccount::factory()->x()->create([ + 'token_expires_at' => now()->subMinutes(5), + 'refresh_token' => 'old_refresh_token', + ]); + + $verifier = new ConnectionVerifier; + + expect(fn () => $verifier->refreshToken($account))->toThrow(TokenExpiredException::class); +}); + +test('bluesky 5xx during refresh raises PlatformUnavailable even when password fallback is stored', function () { + Http::fake([ + // Both endpoints return 5xx — the platform is genuinely down. + 'bsky.social/xrpc/com.atproto.server.refreshSession' => Http::response('upstream timeout', 503), + 'bsky.social/xrpc/com.atproto.server.createSession' => Http::response('upstream timeout', 503), + ]); + + $account = SocialAccount::factory()->bluesky()->create([ + 'token_expires_at' => now()->subMinutes(5), + 'refresh_token' => 'old_refresh_token', + 'meta' => [ + 'service' => 'https://bsky.social', + 'identifier' => 'user.bsky.social', + 'password' => encrypt('app-password'), + ], + ]); + + $verifier = new ConnectionVerifier; + + expect(fn () => $verifier->refreshToken($account))->toThrow(PlatformUnavailableException::class); +}); diff --git a/tests/Feature/VerifyWorkspaceConnectionsTest.php b/tests/Feature/VerifyWorkspaceConnectionsTest.php index 2f351c78..5eaff679 100644 --- a/tests/Feature/VerifyWorkspaceConnectionsTest.php +++ b/tests/Feature/VerifyWorkspaceConnectionsTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use App\Enums\SocialAccount\Status; +use App\Exceptions\PlatformUnavailableException; use App\Exceptions\TokenExpiredException; use App\Jobs\VerifyWorkspaceConnections; use App\Mail\WorkspaceConnectionsDisconnected; @@ -121,6 +122,26 @@ }); }); +test('job does NOT disconnect or email when platform is unavailable', function () { + Mail::fake(); + + $workspace = Workspace::factory()->create(); + $account = SocialAccount::factory()->bluesky()->create(['workspace_id' => $workspace->id]); + + $verifier = mock(ConnectionVerifier::class); + $verifier->shouldReceive('verify')->andThrow( + new PlatformUnavailableException('Bluesky API returned 503 during token refresh', 503) + ); + + app()->instance(ConnectionVerifier::class, $verifier); + + VerifyWorkspaceConnections::dispatch($workspace); + + // Status untouched, no notification email. + expect($account->fresh()->status)->toBe(Status::Connected); + Mail::assertNothingQueued(); +}); + test('job skips already disconnected accounts', function () { Mail::fake(); From d5e28e3d02069fa990a8f8cf4aa682c1a6e5afe3 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 19 May 2026 08:27:46 -0300 Subject: [PATCH 02/14] fix(social): move Mastodon default instance to config + cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups: - verifyMastodon was the last hardcoded host left after the PR moved LinkedIn/YouTube/Bluesky to config. Adds trypost.platforms.mastodon .default_instance (env MASTODON_DEFAULT_INSTANCE) and reads from it. - refreshToken() docblock now declares @throws PlatformUnavailableException (the whole point of the PR was missing from its contract). - Strip the new explanatory comments inside catch blocks and tests — rationale lives in the commit / PR, not inline. The two comments inside empty `catch (TokenExpiredException) {}` blocks stay because there the comment is the only thing telling the reader why the exception is swallowed. --- app/Jobs/RefreshSocialToken.php | 8 -------- app/Jobs/VerifyWorkspaceConnections.php | 2 -- app/Services/Social/ConnectionVerifier.php | 13 ++++--------- config/trypost.php | 2 ++ tests/Feature/Jobs/RefreshSocialTokenTest.php | 1 - .../Services/Social/ConnectionVerifierTest.php | 1 - tests/Feature/VerifyWorkspaceConnectionsTest.php | 1 - 7 files changed, 6 insertions(+), 22 deletions(-) diff --git a/app/Jobs/RefreshSocialToken.php b/app/Jobs/RefreshSocialToken.php index 86957f2d..25b6b1bf 100644 --- a/app/Jobs/RefreshSocialToken.php +++ b/app/Jobs/RefreshSocialToken.php @@ -26,20 +26,12 @@ public function handle(ConnectionVerifier $verifier): void try { $verifier->refreshToken($this->account); } catch (PlatformUnavailableException $e) { - // Platform is down (5xx / network). Leave the account alone — - // next scheduled run will try again. Critically, do NOT mark - // the account expired: that would trigger a false-positive - // "reconnect your account" notification. Log::warning('Token refresh skipped: platform unavailable', [ 'account_id' => $this->account->id, 'platform' => $this->account->platform->value, 'error' => $e->getMessage(), ]); } catch (TokenExpiredException $e) { - // refresh_token rejected by the provider (revoked / rotated / - // expired beyond refresh). Mark the account so the user is - // notified immediately instead of waiting for the next failed - // publish or the daily verify pass. $this->account->markAsTokenExpired($e->getMessage()); } catch (Throwable $e) { Log::warning('Proactive token refresh failed', [ diff --git a/app/Jobs/VerifyWorkspaceConnections.php b/app/Jobs/VerifyWorkspaceConnections.php index 5174b13d..6d01c957 100644 --- a/app/Jobs/VerifyWorkspaceConnections.php +++ b/app/Jobs/VerifyWorkspaceConnections.php @@ -66,8 +66,6 @@ private function verifyAccount(ConnectionVerifier $verifier, SocialAccount $acco return true; } catch (PlatformUnavailableException $e) { - // Platform is down (5xx / network). The account's token is not - // provably bad — skip silently and try again next pass. Log::warning('Social account verification skipped: platform unavailable', [ 'account_id' => $account->id, 'platform' => $account->platform->value, diff --git a/app/Services/Social/ConnectionVerifier.php b/app/Services/Social/ConnectionVerifier.php index e5c9fc17..d13bb620 100644 --- a/app/Services/Social/ConnectionVerifier.php +++ b/app/Services/Social/ConnectionVerifier.php @@ -28,9 +28,6 @@ public function verify(SocialAccount $account): bool // LinkedIn, etc.) invalidate the previous refresh_token on each // refresh, so proactive refreshes during races cause false-positive // disconnects even though the access_token still works fine. - // - // PlatformUnavailableException from refresh propagates naturally so - // the caller can distinguish "platform is down" from "token bad". if ($account->is_token_expired) { $this->refreshToken($account); @@ -76,9 +73,10 @@ private function callVerifyEndpoint(SocialAccount $account): bool * Refresh the account's token via the platform-specific OAuth flow. * Callers that want the smart "try access_token first" behavior should * use verify() instead. This method always attempts a refresh under - * the per-account lock and throws TokenExpiredException on failure. + * the per-account lock. * - * @throws TokenExpiredException if refresh fails + * @throws TokenExpiredException if refresh is rejected by the provider (4xx) + * @throws PlatformUnavailableException if the platform is unreachable (5xx / network) */ public function refreshToken(SocialAccount $account): void { @@ -168,9 +166,6 @@ private function refreshBlueskyToken(SocialAccount $account): void $service = $account->meta['service'] ?? config('trypost.platforms.bluesky.default_service'); $client = TokenRefreshClient::for(Platform::Bluesky); - // Try refresh token first. Connection errors / 5xx surface as - // PlatformUnavailableException (Bluesky is down — don't touch the - // account's status). 4xx falls through to the re-auth fallback. try { $response = $client->send(fn () => Http::withToken($account->refresh_token) ->post("{$service}/xrpc/com.atproto.server.refreshSession")); @@ -512,7 +507,7 @@ private function verifyBluesky(SocialAccount $account): bool private function verifyMastodon(SocialAccount $account): bool { - $instance = $account->meta['instance'] ?? 'https://mastodon.social'; + $instance = $account->meta['instance'] ?? config('trypost.platforms.mastodon.default_instance'); $response = Http::withToken($account->access_token) ->get("{$instance}/api/v1/accounts/verify_credentials"); diff --git a/config/trypost.php b/config/trypost.php index 29fcfb95..45f1d93d 100644 --- a/config/trypost.php +++ b/config/trypost.php @@ -116,6 +116,8 @@ ], 'mastodon' => [ 'enabled' => env('MASTODON_ENABLED', true), + // Default instance used when the account has no `meta.instance` override. + 'default_instance' => env('MASTODON_DEFAULT_INSTANCE', 'https://mastodon.social'), ], ], diff --git a/tests/Feature/Jobs/RefreshSocialTokenTest.php b/tests/Feature/Jobs/RefreshSocialTokenTest.php index 172076a7..c9a9226d 100644 --- a/tests/Feature/Jobs/RefreshSocialTokenTest.php +++ b/tests/Feature/Jobs/RefreshSocialTokenTest.php @@ -86,7 +86,6 @@ (new RefreshSocialToken($this->account))->handle($verifier); - // Critically: account status stays Connected, no notification dispatched. expect($this->account->fresh()->status)->toBe(Status::Connected); Queue::assertNotPushed(SendNotification::class); }); diff --git a/tests/Feature/Services/Social/ConnectionVerifierTest.php b/tests/Feature/Services/Social/ConnectionVerifierTest.php index 9d025385..8488a972 100644 --- a/tests/Feature/Services/Social/ConnectionVerifierTest.php +++ b/tests/Feature/Services/Social/ConnectionVerifierTest.php @@ -411,7 +411,6 @@ test('bluesky 5xx during refresh raises PlatformUnavailable even when password fallback is stored', function () { Http::fake([ - // Both endpoints return 5xx — the platform is genuinely down. 'bsky.social/xrpc/com.atproto.server.refreshSession' => Http::response('upstream timeout', 503), 'bsky.social/xrpc/com.atproto.server.createSession' => Http::response('upstream timeout', 503), ]); diff --git a/tests/Feature/VerifyWorkspaceConnectionsTest.php b/tests/Feature/VerifyWorkspaceConnectionsTest.php index 5eaff679..19976701 100644 --- a/tests/Feature/VerifyWorkspaceConnectionsTest.php +++ b/tests/Feature/VerifyWorkspaceConnectionsTest.php @@ -137,7 +137,6 @@ VerifyWorkspaceConnections::dispatch($workspace); - // Status untouched, no notification email. expect($account->fresh()->status)->toBe(Status::Connected); Mail::assertNothingQueued(); }); From 166008422788b23ec2a4820cf9986f1520bc95fa Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 19 May 2026 08:34:35 -0300 Subject: [PATCH 03/14] refactor(social): use config for OAuth hosts everywhere MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Earlier in the PR the new configs (linkedin.oauth_api, youtube.oauth_api, bluesky.default_service, mastodon.default_instance) were only read by ConnectionVerifier. The same URLs were still hardcoded in the publishers, analytics and the Bluesky auth controller — meaning a self-hosted user setting BLUESKY_DEFAULT_SERVICE or MASTODON_DEFAULT_INSTANCE in env would get split behavior: refresh/verify honor the override, publish/analytics don't. Routes all 10 remaining call sites through the same config values so the overrides actually work end-to-end. --- app/Http/Controllers/Auth/BlueskyController.php | 2 +- app/Services/Social/BlueskyPublisher.php | 4 ++-- app/Services/Social/LinkedInPageAnalytics.php | 2 +- app/Services/Social/LinkedInPagePublisher.php | 2 +- app/Services/Social/LinkedInPublisher.php | 2 +- app/Services/Social/MastodonAnalytics.php | 2 +- app/Services/Social/MastodonPublisher.php | 2 +- app/Services/Social/YouTubeAnalytics.php | 2 +- app/Services/Social/YouTubePublisher.php | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/Auth/BlueskyController.php b/app/Http/Controllers/Auth/BlueskyController.php index 8ec10b91..4a74d99a 100644 --- a/app/Http/Controllers/Auth/BlueskyController.php +++ b/app/Http/Controllers/Auth/BlueskyController.php @@ -53,7 +53,7 @@ public function store(Request $request): View|RedirectResponse $this->authorize('manageAccounts', $workspace); - $service = 'https://bsky.social'; + $service = config('trypost.platforms.bluesky.default_service'); try { // Authenticate with Bluesky diff --git a/app/Services/Social/BlueskyPublisher.php b/app/Services/Social/BlueskyPublisher.php index f545e2df..ec18db80 100644 --- a/app/Services/Social/BlueskyPublisher.php +++ b/app/Services/Social/BlueskyPublisher.php @@ -26,7 +26,7 @@ public function publish(PostPlatform $postPlatform): array $content = $postPlatform->post->content ? app(ContentSanitizer::class)->sanitize($postPlatform->post->content, $postPlatform->platform) : null; $account = $postPlatform->socialAccount; - $service = $account->meta['service'] ?? 'https://bsky.social'; + $service = $account->meta['service'] ?? config('trypost.platforms.bluesky.default_service'); // Refresh token if needed if ($account->is_token_expired || $account->is_token_expiring_soon) { @@ -270,7 +270,7 @@ private function buildPostUrl(string $handle, string $postId): string public function refreshToken(SocialAccount $account): void { - $service = $account->meta['service'] ?? 'https://bsky.social'; + $service = $account->meta['service'] ?? config('trypost.platforms.bluesky.default_service'); // Try refresh first $response = $this->socialHttp()->withToken($account->refresh_token) diff --git a/app/Services/Social/LinkedInPageAnalytics.php b/app/Services/Social/LinkedInPageAnalytics.php index 8c809b9a..1c2c5b6a 100644 --- a/app/Services/Social/LinkedInPageAnalytics.php +++ b/app/Services/Social/LinkedInPageAnalytics.php @@ -240,7 +240,7 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for LinkedIn Page account'); } - $response = Http::asForm()->post('https://www.linkedin.com/oauth/v2/accessToken', [ + $response = Http::asForm()->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, 'client_id' => config('services.linkedin-openid.client_id'), diff --git a/app/Services/Social/LinkedInPagePublisher.php b/app/Services/Social/LinkedInPagePublisher.php index 7c7ed796..d4a3c691 100644 --- a/app/Services/Social/LinkedInPagePublisher.php +++ b/app/Services/Social/LinkedInPagePublisher.php @@ -458,7 +458,7 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for LinkedIn Page account'); } - $response = Http::asForm()->post('https://www.linkedin.com/oauth/v2/accessToken', [ + $response = Http::asForm()->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, 'client_id' => config('services.linkedin-openid.client_id'), diff --git a/app/Services/Social/LinkedInPublisher.php b/app/Services/Social/LinkedInPublisher.php index 37bdd435..540e37ae 100644 --- a/app/Services/Social/LinkedInPublisher.php +++ b/app/Services/Social/LinkedInPublisher.php @@ -439,7 +439,7 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for LinkedIn account'); } - $response = Http::asForm()->post('https://www.linkedin.com/oauth/v2/accessToken', [ + $response = Http::asForm()->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, 'client_id' => config('services.linkedin.client_id'), diff --git a/app/Services/Social/MastodonAnalytics.php b/app/Services/Social/MastodonAnalytics.php index e071fbeb..606ab8a0 100644 --- a/app/Services/Social/MastodonAnalytics.php +++ b/app/Services/Social/MastodonAnalytics.php @@ -20,7 +20,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array return ['unsupported' => true, 'reason' => 'missing_post_id']; } - $instance = data_get($account->meta, 'instance', 'https://mastodon.social'); + $instance = data_get($account->meta, 'instance', config('trypost.platforms.mastodon.default_instance')); // Public posts: no auth needed. Our token only requests write scopes // (read:accounts + write:statuses + write:media), so attaching the diff --git a/app/Services/Social/MastodonPublisher.php b/app/Services/Social/MastodonPublisher.php index 66ac4b49..218d099d 100644 --- a/app/Services/Social/MastodonPublisher.php +++ b/app/Services/Social/MastodonPublisher.php @@ -25,7 +25,7 @@ public function publish(PostPlatform $postPlatform): array $content = $postPlatform->post->content ? app(ContentSanitizer::class)->sanitize($postPlatform->post->content, $postPlatform->platform) : null; $account = $postPlatform->socialAccount; - $instance = $account->meta['instance'] ?? 'https://mastodon.social'; + $instance = $account->meta['instance'] ?? config('trypost.platforms.mastodon.default_instance'); $medias = $postPlatform->post->mediaItems; $mediaIds = []; diff --git a/app/Services/Social/YouTubeAnalytics.php b/app/Services/Social/YouTubeAnalytics.php index 0d42997e..41f6a90c 100644 --- a/app/Services/Social/YouTubeAnalytics.php +++ b/app/Services/Social/YouTubeAnalytics.php @@ -168,7 +168,7 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for YouTube account'); } - $response = Http::asForm()->post('https://oauth2.googleapis.com/token', [ + $response = Http::asForm()->post(config('trypost.platforms.youtube.oauth_api').'/token', [ 'client_id' => config('services.google.client_id'), 'client_secret' => config('services.google.client_secret'), 'grant_type' => 'refresh_token', diff --git a/app/Services/Social/YouTubePublisher.php b/app/Services/Social/YouTubePublisher.php index 7525baa7..1f67c6d0 100644 --- a/app/Services/Social/YouTubePublisher.php +++ b/app/Services/Social/YouTubePublisher.php @@ -229,7 +229,7 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for YouTube account'); } - $response = Http::asForm()->post('https://oauth2.googleapis.com/token', [ + $response = Http::asForm()->post(config('trypost.platforms.youtube.oauth_api').'/token', [ 'client_id' => config('services.google.client_id'), 'client_secret' => config('services.google.client_secret'), 'grant_type' => 'refresh_token', From 090cc761dd86f91cee6a01ef0871ae1228d3473a Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 19 May 2026 08:42:02 -0300 Subject: [PATCH 04/14] test(social): smoke tests for analytics refactored in this PR LinkedInPageAnalytics and MastodonAnalytics were the only two of the 11 files refactored to read OAuth host / default instance from config that had zero test coverage. Adds smoke tests that assert the HTTP request hits the configured URL, so a typo in the config key (e.g. linkedin.api vs linkedin.oauth_api) would now fail loudly. --- .../Social/LinkedInPageAnalyticsTest.php | 55 ++++++++++++++ .../Services/Social/MastodonAnalyticsTest.php | 75 +++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 tests/Feature/Services/Social/LinkedInPageAnalyticsTest.php create mode 100644 tests/Feature/Services/Social/MastodonAnalyticsTest.php diff --git a/tests/Feature/Services/Social/LinkedInPageAnalyticsTest.php b/tests/Feature/Services/Social/LinkedInPageAnalyticsTest.php new file mode 100644 index 00000000..0836940d --- /dev/null +++ b/tests/Feature/Services/Social/LinkedInPageAnalyticsTest.php @@ -0,0 +1,55 @@ +user = User::factory()->create(); + $this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]); + $this->socialAccount = SocialAccount::factory()->linkedinPage()->create([ + 'workspace_id' => $this->workspace->id, + 'token_expires_at' => now()->subHour(), + 'refresh_token' => 'old_refresh_token', + ]); + $this->post = Post::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'user_id' => $this->user->id, + ]); + $this->postPlatform = PostPlatform::factory()->create([ + 'post_id' => $this->post->id, + 'social_account_id' => $this->socialAccount->id, + 'platform' => Platform::LinkedInPage, + 'content_type' => ContentType::LinkedInPagePost, + 'platform_post_id' => 'urn:li:share:1234567890', + ]); +}); + +test('linkedin page analytics refresh hits the configured oauth host', function () { + Http::fake([ + 'www.linkedin.com/oauth/v2/accessToken' => Http::response([ + 'access_token' => 'new_token', + 'refresh_token' => 'new_refresh_token', + 'expires_in' => 5184000, + ], 200), + 'api.linkedin.com/rest/socialActions/*' => Http::response([ + 'likesSummary' => ['totalLikes' => 0], + 'commentsSummary' => ['aggregatedTotalComments' => 0], + ], 200), + ]); + + (new LinkedInPageAnalytics)->fetchPostMetrics($this->postPlatform); + + Http::assertSent(fn ($request) => str_contains( + $request->url(), + rtrim((string) config('trypost.platforms.linkedin.oauth_api'), '/').'/oauth/v2/accessToken' + )); +}); diff --git a/tests/Feature/Services/Social/MastodonAnalyticsTest.php b/tests/Feature/Services/Social/MastodonAnalyticsTest.php new file mode 100644 index 00000000..0a6fe7cc --- /dev/null +++ b/tests/Feature/Services/Social/MastodonAnalyticsTest.php @@ -0,0 +1,75 @@ +user = User::factory()->create(); + $this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]); + $this->post = Post::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'user_id' => $this->user->id, + ]); +}); + +test('mastodon analytics falls back to configured default instance', function () { + $defaultInstance = (string) config('trypost.platforms.mastodon.default_instance'); + + Http::fake([ + "{$defaultInstance}/api/v1/statuses/*" => Http::response([ + 'favourites_count' => 5, + 'reblogs_count' => 2, + 'replies_count' => 1, + ], 200), + ]); + + $account = SocialAccount::factory()->mastodon()->create([ + 'workspace_id' => $this->workspace->id, + 'meta' => [], + ]); + $postPlatform = PostPlatform::factory()->create([ + 'post_id' => $this->post->id, + 'social_account_id' => $account->id, + 'platform' => Platform::Mastodon, + 'content_type' => ContentType::MastodonPost, + 'platform_post_id' => '109876543210', + ]); + + $metrics = (new MastodonAnalytics)->fetchPostMetrics($postPlatform); + + expect($metrics)->toBeArray(); + Http::assertSent(fn ($request) => str_starts_with($request->url(), $defaultInstance.'/api/v1/statuses/')); +}); + +test('mastodon analytics honors per-account instance override', function () { + Http::fake([ + 'techhub.social/api/v1/statuses/*' => Http::response([ + 'favourites_count' => 0, 'reblogs_count' => 0, 'replies_count' => 0, + ], 200), + ]); + + $account = SocialAccount::factory()->mastodon()->create([ + 'workspace_id' => $this->workspace->id, + 'meta' => ['instance' => 'https://techhub.social'], + ]); + $postPlatform = PostPlatform::factory()->create([ + 'post_id' => $this->post->id, + 'social_account_id' => $account->id, + 'platform' => Platform::Mastodon, + 'content_type' => ContentType::MastodonPost, + 'platform_post_id' => '999', + ]); + + (new MastodonAnalytics)->fetchPostMetrics($postPlatform); + + Http::assertSent(fn ($request) => str_starts_with($request->url(), 'https://techhub.social/api/v1/statuses/')); +}); From 32e1f89adb43dfd1c18278a506de1a797db4f592 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 19 May 2026 09:01:02 -0300 Subject: [PATCH 05/14] fix(social): publish flow honors PlatformUnavailable too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commits in this PR closed the loophole on the hourly / daily token-refresh jobs. The same loophole remained on the publish path: every per-platform publisher (LinkedIn, X, YouTube, TikTok, Threads, Instagram, Pinterest, Bluesky and their Analytics siblings) has its own refreshToken() called before publishing a scheduled post, and all of those treated any non-2xx as TokenExpired — including 5xx. Result before this commit: a Bluesky outage that coincided with a scheduled publish would mark the account as expired and fail the post. Changes: - Route every refreshToken() in the 16 publisher / analytics classes through TokenRefreshClient::for(Platform::X)->send(...). - TokenRefreshClient now also fills platformErrorCode from the HTTP status and pulls error_description / error.message from the JSON body, preserving the richer info LinkedIn / X / TikTok / Pinterest / Threads used to put on their TokenExpiredException. - PublishToSocialPlatform catches PlatformUnavailableException explicitly: the post is marked failed (category: platform_unavailable, with http_status in error_context) but the account stays Connected. No retry inside this job — the scheduler reattempts the next run. Test added: publish flow does NOT mark account expired when the publisher throws PlatformUnavailable. Full suite: 1569 passing. --- app/Jobs/PublishToSocialPlatform.php | 14 ++++++ app/Services/Social/BlueskyPublisher.php | 46 ++++++++----------- app/Services/Social/InstagramAnalytics.php | 9 +--- app/Services/Social/InstagramPublisher.php | 11 +---- app/Services/Social/LinkedInPageAnalytics.php | 19 ++++---- app/Services/Social/LinkedInPagePublisher.php | 20 +++----- app/Services/Social/LinkedInPublisher.php | 20 +++----- app/Services/Social/PinterestAnalytics.php | 10 ++-- app/Services/Social/PinterestPublisher.php | 12 +---- app/Services/Social/ThreadsAnalytics.php | 11 ++--- app/Services/Social/ThreadsPublisher.php | 13 ++---- app/Services/Social/TikTokAnalytics.php | 19 ++++---- app/Services/Social/TikTokCreatorInfo.php | 19 ++++---- app/Services/Social/TikTokPublisher.php | 21 ++++----- app/Services/Social/TokenRefreshClient.php | 8 +++- app/Services/Social/XAnalytics.php | 10 ++-- app/Services/Social/XPublisher.php | 11 +---- app/Services/Social/YouTubeAnalytics.php | 20 ++++---- app/Services/Social/YouTubePublisher.php | 20 ++++---- .../Jobs/PublishToSocialPlatformTest.php | 23 ++++++++++ 20 files changed, 144 insertions(+), 192 deletions(-) diff --git a/app/Jobs/PublishToSocialPlatform.php b/app/Jobs/PublishToSocialPlatform.php index dd0128ea..0ee2b719 100644 --- a/app/Jobs/PublishToSocialPlatform.php +++ b/app/Jobs/PublishToSocialPlatform.php @@ -10,6 +10,7 @@ use App\Enums\SocialAccount\Platform as SocialPlatform; use App\Enums\SocialAccount\Status; use App\Events\PostPlatformStatusUpdated; +use App\Exceptions\PlatformUnavailableException; use App\Exceptions\Social\SocialPublishException; use App\Exceptions\TokenExpiredException; use App\Mail\PostPublished; @@ -110,6 +111,19 @@ public function handle(): void $result = $publisher->publish($this->postPlatform); $this->postPlatform->markAsPublished(data_get($result, 'id'), data_get($result, 'url')); break; + } catch (PlatformUnavailableException $e) { + Log::warning('Publish skipped: platform unavailable', [ + 'post_platform_id' => $this->postPlatform->id, + 'platform' => $this->postPlatform->platform->value, + 'error' => $e->getMessage(), + ]); + + $this->postPlatform->markAsFailed($e->getMessage(), [ + 'category' => 'platform_unavailable', + 'http_status' => $e->httpStatus, + 'failed_at' => now()->toIso8601String(), + ]); + break; } catch (TokenExpiredException $e) { if ($attempt < $maxAttempts) { try { diff --git a/app/Services/Social/BlueskyPublisher.php b/app/Services/Social/BlueskyPublisher.php index ec18db80..68c5fc15 100644 --- a/app/Services/Social/BlueskyPublisher.php +++ b/app/Services/Social/BlueskyPublisher.php @@ -271,12 +271,12 @@ private function buildPostUrl(string $handle, string $postId): string public function refreshToken(SocialAccount $account): void { $service = $account->meta['service'] ?? config('trypost.platforms.bluesky.default_service'); + $client = TokenRefreshClient::for(Platform::Bluesky); - // Try refresh first - $response = $this->socialHttp()->withToken($account->refresh_token) - ->post("{$service}/xrpc/com.atproto.server.refreshSession"); + try { + $response = $client->send(fn () => $this->socialHttp()->withToken($account->refresh_token) + ->post("{$service}/xrpc/com.atproto.server.refreshSession")); - if ($response->successful()) { $data = $response->json(); $account->update([ 'access_token' => data_get($data, 'accessJwt'), @@ -285,37 +285,27 @@ public function refreshToken(SocialAccount $account): void ]); return; + } catch (TokenExpiredException) { + // refresh token rejected (4xx) — fall back to re-auth below } - Log::warning('Bluesky refresh token failed, trying re-authentication', [ - 'status' => $response->status(), - ]); - - // If refresh fails, re-authenticate with stored credentials if (isset($account->meta['password'])) { try { - $password = decrypt($account->meta['password']); - $identifier = $account->meta['identifier']; + $reauth = $client->send(fn () => Http::post("{$service}/xrpc/com.atproto.server.createSession", [ + 'identifier' => $account->meta['identifier'], + 'password' => decrypt($account->meta['password']), + ])); - $response = Http::post("{$service}/xrpc/com.atproto.server.createSession", [ - 'identifier' => $identifier, - 'password' => $password, + $data = $reauth->json(); + $account->update([ + 'access_token' => data_get($data, 'accessJwt'), + 'refresh_token' => data_get($data, 'refreshJwt'), + 'token_expires_at' => now()->addHours(2), ]); - if ($response->successful()) { - $data = $response->json(); - $account->update([ - 'access_token' => data_get($data, 'accessJwt'), - 'refresh_token' => data_get($data, 'refreshJwt'), - 'token_expires_at' => now()->addHours(2), - ]); - - return; - } - } catch (\Exception $e) { - Log::error('Bluesky re-authentication failed', [ - 'error' => $e->getMessage(), - ]); + return; + } catch (TokenExpiredException) { + // re-auth rejected with stored credentials — fall through } } diff --git a/app/Services/Social/InstagramAnalytics.php b/app/Services/Social/InstagramAnalytics.php index 3b98fa44..126a4626 100644 --- a/app/Services/Social/InstagramAnalytics.php +++ b/app/Services/Social/InstagramAnalytics.php @@ -211,15 +211,10 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for Instagram account'); } - $response = Http::get(config('trypost.platforms.instagram.auth_api').'/refresh_access_token', [ + $response = TokenRefreshClient::for(Platform::Instagram)->send(fn () => Http::get(config('trypost.platforms.instagram.auth_api').'/refresh_access_token', [ 'grant_type' => 'ig_refresh_token', 'access_token' => $account->access_token, - ]); - - if ($response->failed()) { - Log::error('Instagram token refresh failed', ['body' => $this->redactResponseBody($response->body())]); - throw new TokenExpiredException('Instagram token refresh failed'); - } + ])); $data = $response->json(); diff --git a/app/Services/Social/InstagramPublisher.php b/app/Services/Social/InstagramPublisher.php index 85c8b0fb..9ad27dd5 100644 --- a/app/Services/Social/InstagramPublisher.php +++ b/app/Services/Social/InstagramPublisher.php @@ -8,7 +8,6 @@ use App\Enums\SocialAccount\Platform; use App\Exceptions\Social\ErrorCategory; use App\Exceptions\Social\InstagramPublishException; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Media\MediaOptimizer; @@ -403,16 +402,10 @@ private function refreshToken(SocialAccount $account): void return; } - $response = Http::get(config('trypost.platforms.instagram.auth_api').'/refresh_access_token', [ + $response = TokenRefreshClient::for(Platform::Instagram)->send(fn () => Http::get(config('trypost.platforms.instagram.auth_api').'/refresh_access_token', [ 'grant_type' => 'ig_refresh_token', 'access_token' => $account->access_token, - ]); - - if ($response->failed()) { - Log::error('Instagram token refresh failed', ['body' => $this->redactResponseBody($response->body())]); - - throw new TokenExpiredException('Failed to refresh Instagram token'); - } + ])); $data = $response->json(); $newToken = data_get($data, 'access_token'); diff --git a/app/Services/Social/LinkedInPageAnalytics.php b/app/Services/Social/LinkedInPageAnalytics.php index 1c2c5b6a..98c1d519 100644 --- a/app/Services/Social/LinkedInPageAnalytics.php +++ b/app/Services/Social/LinkedInPageAnalytics.php @@ -4,6 +4,7 @@ namespace App\Services\Social; +use App\Enums\SocialAccount\Platform; use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; @@ -240,17 +241,13 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for LinkedIn Page account'); } - $response = Http::asForm()->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - 'client_id' => config('services.linkedin-openid.client_id'), - 'client_secret' => config('services.linkedin-openid.client_secret'), - ]); - - if ($response->failed()) { - Log::error('LinkedIn token refresh failed', ['body' => $this->redactResponseBody($response->body())]); - throw new TokenExpiredException('LinkedIn token refresh failed'); - } + $response = TokenRefreshClient::for(Platform::LinkedInPage)->send(fn () => Http::asForm() + ->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ + 'grant_type' => 'refresh_token', + 'refresh_token' => $account->refresh_token, + 'client_id' => config('services.linkedin-openid.client_id'), + 'client_secret' => config('services.linkedin-openid.client_secret'), + ])); $data = $response->json(); diff --git a/app/Services/Social/LinkedInPagePublisher.php b/app/Services/Social/LinkedInPagePublisher.php index d4a3c691..7486f5d3 100644 --- a/app/Services/Social/LinkedInPagePublisher.php +++ b/app/Services/Social/LinkedInPagePublisher.php @@ -458,19 +458,13 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for LinkedIn Page account'); } - $response = Http::asForm()->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - 'client_id' => config('services.linkedin-openid.client_id'), - 'client_secret' => config('services.linkedin-openid.client_secret'), - ]); - - if ($response->failed()) { - throw new TokenExpiredException( - message: data_get($response->json(), 'error_description', 'Failed to refresh LinkedIn Page token'), - platformErrorCode: (string) $response->status(), - ); - } + $response = TokenRefreshClient::for(Platform::LinkedInPage)->send(fn () => Http::asForm() + ->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ + 'grant_type' => 'refresh_token', + 'refresh_token' => $account->refresh_token, + 'client_id' => config('services.linkedin-openid.client_id'), + 'client_secret' => config('services.linkedin-openid.client_secret'), + ])); $data = $response->json(); diff --git a/app/Services/Social/LinkedInPublisher.php b/app/Services/Social/LinkedInPublisher.php index 540e37ae..ed6d230c 100644 --- a/app/Services/Social/LinkedInPublisher.php +++ b/app/Services/Social/LinkedInPublisher.php @@ -439,19 +439,13 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for LinkedIn account'); } - $response = Http::asForm()->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - 'client_id' => config('services.linkedin.client_id'), - 'client_secret' => config('services.linkedin.client_secret'), - ]); - - if ($response->failed()) { - throw new TokenExpiredException( - message: data_get($response->json(), 'error_description', 'Failed to refresh LinkedIn token'), - platformErrorCode: (string) $response->status(), - ); - } + $response = TokenRefreshClient::for(Platform::LinkedIn)->send(fn () => Http::asForm() + ->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ + 'grant_type' => 'refresh_token', + 'refresh_token' => $account->refresh_token, + 'client_id' => config('services.linkedin.client_id'), + 'client_secret' => config('services.linkedin.client_secret'), + ])); $data = $response->json(); diff --git a/app/Services/Social/PinterestAnalytics.php b/app/Services/Social/PinterestAnalytics.php index 78745fb6..2a343b9e 100644 --- a/app/Services/Social/PinterestAnalytics.php +++ b/app/Services/Social/PinterestAnalytics.php @@ -4,6 +4,7 @@ namespace App\Services\Social; +use App\Enums\SocialAccount\Platform; use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; @@ -167,18 +168,13 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for Pinterest account'); } - $response = Http::withBasicAuth( + $response = TokenRefreshClient::for(Platform::Pinterest)->send(fn () => Http::withBasicAuth( config('services.pinterest.client_id'), config('services.pinterest.client_secret'), )->asForm()->post(config('trypost.platforms.pinterest.api').'/oauth/token', [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, - ]); - - if ($response->failed()) { - Log::error('Pinterest token refresh failed', ['body' => $this->redactResponseBody($response->body())]); - throw new TokenExpiredException('Pinterest token refresh failed'); - } + ])); $data = $response->json(); diff --git a/app/Services/Social/PinterestPublisher.php b/app/Services/Social/PinterestPublisher.php index ed6c4364..116b93f5 100644 --- a/app/Services/Social/PinterestPublisher.php +++ b/app/Services/Social/PinterestPublisher.php @@ -8,7 +8,6 @@ use App\Enums\SocialAccount\Platform; use App\Exceptions\Social\ErrorCategory; use App\Exceptions\Social\PinterestPublishException; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Media\MediaOptimizer; @@ -403,7 +402,7 @@ private function waitForMediaProcessing(SocialAccount $account, string $mediaId, private function refreshToken(SocialAccount $account): void { - $response = Http::asForm() + $response = TokenRefreshClient::for(Platform::Pinterest)->send(fn () => Http::asForm() ->withBasicAuth( config('services.pinterest.client_id'), config('services.pinterest.client_secret') @@ -411,14 +410,7 @@ private function refreshToken(SocialAccount $account): void ->post($this->baseUrl.'/oauth/token', [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, - ]); - - if ($response->failed()) { - throw new TokenExpiredException( - message: data_get($response->json(), 'error_description', 'Failed to refresh Pinterest token'), - platformErrorCode: (string) $response->status(), - ); - } + ])); $data = $response->json(); diff --git a/app/Services/Social/ThreadsAnalytics.php b/app/Services/Social/ThreadsAnalytics.php index a34fd8f5..dc245f63 100644 --- a/app/Services/Social/ThreadsAnalytics.php +++ b/app/Services/Social/ThreadsAnalytics.php @@ -4,7 +4,7 @@ namespace App\Services\Social; -use App\Exceptions\TokenExpiredException; +use App\Enums\SocialAccount\Platform; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; @@ -141,15 +141,10 @@ private function getHttpClient(): PendingRequest private function refreshToken(SocialAccount $account): void { - $response = Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [ + $response = TokenRefreshClient::for(Platform::Threads)->send(fn () => Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [ 'grant_type' => 'th_refresh_token', 'access_token' => $account->access_token, - ]); - - if ($response->failed()) { - Log::error('Threads token refresh failed', ['body' => $this->redactResponseBody($response->body())]); - throw new TokenExpiredException('Threads token refresh failed'); - } + ])); $data = $response->json(); diff --git a/app/Services/Social/ThreadsPublisher.php b/app/Services/Social/ThreadsPublisher.php index d799fa68..135f9dd4 100644 --- a/app/Services/Social/ThreadsPublisher.php +++ b/app/Services/Social/ThreadsPublisher.php @@ -4,8 +4,8 @@ namespace App\Services\Social; +use App\Enums\SocialAccount\Platform; use App\Exceptions\Social\ThreadsPublishException; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; @@ -306,17 +306,10 @@ private function waitForMediaProcessing(string $containerId, string $accessToken private function refreshToken(SocialAccount $account): void { // Threads uses long-lived tokens that can be refreshed - $response = Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [ + $response = TokenRefreshClient::for(Platform::Threads)->send(fn () => Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [ 'grant_type' => 'th_refresh_token', 'access_token' => $account->access_token, - ]); - - if ($response->failed()) { - throw new TokenExpiredException( - message: data_get($response->json(), 'error.message', 'Failed to refresh Threads token'), - platformErrorCode: (string) $response->status(), - ); - } + ])); $data = $response->json(); diff --git a/app/Services/Social/TikTokAnalytics.php b/app/Services/Social/TikTokAnalytics.php index 7d30333d..7532ae3c 100644 --- a/app/Services/Social/TikTokAnalytics.php +++ b/app/Services/Social/TikTokAnalytics.php @@ -4,6 +4,7 @@ namespace App\Services\Social; +use App\Enums\SocialAccount\Platform; use App\Exceptions\TokenExpiredException; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; @@ -166,17 +167,13 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for TikTok account'); } - $response = Http::asForm()->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [ - 'client_key' => config('services.tiktok.client_id'), - 'client_secret' => config('services.tiktok.client_secret'), - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ]); - - if ($response->failed()) { - Log::error('TikTok token refresh failed', ['body' => $this->redactResponseBody($response->body())]); - throw new TokenExpiredException('TikTok token refresh failed'); - } + $response = TokenRefreshClient::for(Platform::TikTok)->send(fn () => Http::asForm() + ->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [ + 'client_key' => config('services.tiktok.client_id'), + 'client_secret' => config('services.tiktok.client_secret'), + 'grant_type' => 'refresh_token', + 'refresh_token' => $account->refresh_token, + ])); $data = $response->json(); diff --git a/app/Services/Social/TikTokCreatorInfo.php b/app/Services/Social/TikTokCreatorInfo.php index 42a4858d..d3fe8ab5 100644 --- a/app/Services/Social/TikTokCreatorInfo.php +++ b/app/Services/Social/TikTokCreatorInfo.php @@ -4,6 +4,7 @@ namespace App\Services\Social; +use App\Enums\SocialAccount\Platform; use App\Exceptions\TokenExpiredException; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; @@ -130,17 +131,13 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for TikTok account'); } - $response = Http::asForm()->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [ - 'client_key' => config('services.tiktok.client_id'), - 'client_secret' => config('services.tiktok.client_secret'), - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ]); - - if ($response->failed()) { - Log::error('TikTok token refresh failed', ['body' => $this->redactResponseBody($response->body())]); - throw new TokenExpiredException('TikTok token refresh failed'); - } + $response = TokenRefreshClient::for(Platform::TikTok)->send(fn () => Http::asForm() + ->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [ + 'client_key' => config('services.tiktok.client_id'), + 'client_secret' => config('services.tiktok.client_secret'), + 'grant_type' => 'refresh_token', + 'refresh_token' => $account->refresh_token, + ])); $data = $response->json(); diff --git a/app/Services/Social/TikTokPublisher.php b/app/Services/Social/TikTokPublisher.php index 78e4d2d6..c0b875d0 100644 --- a/app/Services/Social/TikTokPublisher.php +++ b/app/Services/Social/TikTokPublisher.php @@ -4,6 +4,7 @@ namespace App\Services\Social; +use App\Enums\SocialAccount\Platform; use App\Exceptions\Social\ErrorCategory; use App\Exceptions\Social\TikTokPublishException; use App\Exceptions\TokenExpiredException; @@ -322,19 +323,13 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for TikTok account'); } - $response = Http::asForm()->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [ - 'client_key' => config('services.tiktok.client_id'), - 'client_secret' => config('services.tiktok.client_secret'), - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ]); - - if ($response->failed()) { - throw new TokenExpiredException( - message: data_get($response->json(), 'error.message', 'Failed to refresh TikTok token'), - platformErrorCode: (string) $response->status(), - ); - } + $response = TokenRefreshClient::for(Platform::TikTok)->send(fn () => Http::asForm() + ->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [ + 'client_key' => config('services.tiktok.client_id'), + 'client_secret' => config('services.tiktok.client_secret'), + 'grant_type' => 'refresh_token', + 'refresh_token' => $account->refresh_token, + ])); $data = $response->json(); diff --git a/app/Services/Social/TokenRefreshClient.php b/app/Services/Social/TokenRefreshClient.php index e1959897..05b1c774 100644 --- a/app/Services/Social/TokenRefreshClient.php +++ b/app/Services/Social/TokenRefreshClient.php @@ -60,7 +60,13 @@ public function send(Closure $request): Response Log::error("TokenRefreshClient: {$name} token refresh failed", [ 'body' => $this->redactBody($response->body()), ]); - throw new TokenExpiredException("Failed to refresh {$name} token"); + + $body = $response->json(); + $message = data_get($body, 'error_description') + ?? data_get($body, 'error.message') + ?? "Failed to refresh {$name} token"; + + throw new TokenExpiredException($message, platformErrorCode: (string) $response->status()); } return $response; diff --git a/app/Services/Social/XAnalytics.php b/app/Services/Social/XAnalytics.php index ff8d07ed..4d620260 100644 --- a/app/Services/Social/XAnalytics.php +++ b/app/Services/Social/XAnalytics.php @@ -4,6 +4,7 @@ namespace App\Services\Social; +use App\Enums\SocialAccount\Platform; use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; @@ -206,18 +207,13 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for X account'); } - $response = $this->socialHttp() + $response = TokenRefreshClient::for(Platform::X)->send(fn () => $this->socialHttp() ->withBasicAuth(config('services.x.client_id'), config('services.x.client_secret')) ->asForm() ->post(config('trypost.platforms.x.api').'/oauth2/token', [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, - ]); - - if ($response->failed()) { - Log::error('X token refresh failed', ['body' => $this->redactResponseBody($response->body())]); - throw new TokenExpiredException('X token refresh failed'); - } + ])); $data = $response->json(); diff --git a/app/Services/Social/XPublisher.php b/app/Services/Social/XPublisher.php index ba9ef292..56f1a10a 100644 --- a/app/Services/Social/XPublisher.php +++ b/app/Services/Social/XPublisher.php @@ -335,19 +335,12 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for X account'); } - $response = Http::asForm() + $response = TokenRefreshClient::for(Platform::X)->send(fn () => Http::asForm() ->withBasicAuth(config('services.x.client_id'), config('services.x.client_secret')) ->post("{$this->baseUrl}/oauth2/token", [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, - ]); - - if ($response->failed()) { - throw new TokenExpiredException( - message: data_get($response->json(), 'error_description', 'Failed to refresh X token'), - platformErrorCode: (string) $response->status(), - ); - } + ])); $data = $response->json(); diff --git a/app/Services/Social/YouTubeAnalytics.php b/app/Services/Social/YouTubeAnalytics.php index 41f6a90c..f082667b 100644 --- a/app/Services/Social/YouTubeAnalytics.php +++ b/app/Services/Social/YouTubeAnalytics.php @@ -4,6 +4,7 @@ namespace App\Services\Social; +use App\Enums\SocialAccount\Platform; use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; @@ -168,18 +169,13 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for YouTube account'); } - $response = Http::asForm()->post(config('trypost.platforms.youtube.oauth_api').'/token', [ - 'client_id' => config('services.google.client_id'), - 'client_secret' => config('services.google.client_secret'), - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ]); - - if ($response->failed()) { - Log::error('YouTube token refresh failed', ['body' => $this->redactResponseBody($response->body())]); - - throw new TokenExpiredException('Failed to refresh YouTube token'); - } + $response = TokenRefreshClient::for(Platform::YouTube)->send(fn () => Http::asForm() + ->post(config('trypost.platforms.youtube.oauth_api').'/token', [ + 'client_id' => config('services.google.client_id'), + 'client_secret' => config('services.google.client_secret'), + 'grant_type' => 'refresh_token', + 'refresh_token' => $account->refresh_token, + ])); $data = $response->json(); diff --git a/app/Services/Social/YouTubePublisher.php b/app/Services/Social/YouTubePublisher.php index 1f67c6d0..3840766d 100644 --- a/app/Services/Social/YouTubePublisher.php +++ b/app/Services/Social/YouTubePublisher.php @@ -4,6 +4,7 @@ namespace App\Services\Social; +use App\Enums\SocialAccount\Platform; use App\Exceptions\Social\ErrorCategory; use App\Exceptions\Social\YouTubePublishException; use App\Exceptions\TokenExpiredException; @@ -229,18 +230,13 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for YouTube account'); } - $response = Http::asForm()->post(config('trypost.platforms.youtube.oauth_api').'/token', [ - 'client_id' => config('services.google.client_id'), - 'client_secret' => config('services.google.client_secret'), - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ]); - - if ($response->failed()) { - Log::error('YouTube token refresh failed', ['body' => $this->redactResponseBody($response->body())]); - - throw new TokenExpiredException('Failed to refresh YouTube token'); - } + $response = TokenRefreshClient::for(Platform::YouTube)->send(fn () => Http::asForm() + ->post(config('trypost.platforms.youtube.oauth_api').'/token', [ + 'client_id' => config('services.google.client_id'), + 'client_secret' => config('services.google.client_secret'), + 'grant_type' => 'refresh_token', + 'refresh_token' => $account->refresh_token, + ])); $data = $response->json(); diff --git a/tests/Feature/Jobs/PublishToSocialPlatformTest.php b/tests/Feature/Jobs/PublishToSocialPlatformTest.php index 4f39d5ed..30d4c175 100644 --- a/tests/Feature/Jobs/PublishToSocialPlatformTest.php +++ b/tests/Feature/Jobs/PublishToSocialPlatformTest.php @@ -9,6 +9,7 @@ use App\Events\PostPlatformStatusUpdated; use App\Exceptions\Social\ErrorCategory; use App\Exceptions\Social\LinkedInPublishException; +use App\Exceptions\PlatformUnavailableException; use App\Exceptions\TokenExpiredException; use App\Jobs\PublishToSocialPlatform; use App\Jobs\SendNotification; @@ -109,6 +110,28 @@ expect($this->socialAccount->status)->toBe(AccountStatus::TokenExpired); }); +test('publish to social platform does NOT mark account expired when platform is unavailable', function () { + Event::fake(); + Mail::fake(); + + $publisher = Mockery::mock(LinkedInPublisher::class); + $publisher->shouldReceive('publish')->andThrow( + new PlatformUnavailableException('LinkedIn API returned 503 during token refresh', 503) + ); + + $this->app->instance(LinkedInPublisher::class, $publisher); + + (new PublishToSocialPlatform($this->postPlatform))->handle(); + + $this->postPlatform->refresh(); + $this->socialAccount->refresh(); + + expect($this->postPlatform->status)->toBe(PlatformStatus::Failed); + expect($this->postPlatform->error_context['category'] ?? null)->toBe('platform_unavailable'); + expect($this->postPlatform->error_context['http_status'] ?? null)->toBe(503); + expect($this->socialAccount->status)->toBe(AccountStatus::Connected); +}); + test('publish to social platform updates post status when all platforms finished', function () { Event::fake(); From fcb70b3599f2623fe65042194a00b2e78badeada Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 19 May 2026 09:18:58 -0300 Subject: [PATCH 06/14] refactor(social): single source of truth for token refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every per-platform publisher and analytics class had its own private refreshToken() implementation, all doing essentially the same thing as ConnectionVerifier's per-platform refresh*Token methods. ~17 copies of near-identical OAuth refresh logic across the codebase, which is how the 5xx→TokenExpired bug got planted in the publish path even after we fixed it on the hourly/daily jobs. Consolidation: - All publish/analytics paths call app(ConnectionVerifier::class) ->refreshToken($account) instead of their own implementation. - 17 private refreshToken() methods deleted. - refreshTokenWithLock helper removed from HasSocialHttpClient trait (its lock semantics are duplicated by ConnectionVerifier's per- account lock). - ConnectionVerifier::refreshLinkedInToken passes $account->platform to TokenRefreshClient so the "LinkedIn" vs "LinkedIn Page" label in error messages is preserved. - TokenRefreshClient now treats HTTP 429 the same as 5xx (raises PlatformUnavailableException) — replaces the retry-on-429 behavior that socialHttp() provided to the deleted refresh methods. Net: -460 LOC. Single per-platform refresh implementation. Every fix or new platform now lands in exactly one place. --- app/Services/Social/BlueskyPublisher.php | 47 +------------------ .../Social/Concerns/HasSocialHttpClient.php | 21 --------- app/Services/Social/ConnectionVerifier.php | 4 +- app/Services/Social/InstagramAnalytics.php | 30 +----------- app/Services/Social/InstagramPublisher.php | 27 +---------- app/Services/Social/LinkedInPageAnalytics.php | 30 +----------- app/Services/Social/LinkedInPagePublisher.php | 30 +----------- app/Services/Social/LinkedInPublisher.php | 30 +----------- app/Services/Social/PinterestAnalytics.php | 30 +----------- app/Services/Social/PinterestPublisher.php | 25 +--------- app/Services/Social/ThreadsAnalytics.php | 21 +-------- app/Services/Social/ThreadsPublisher.php | 24 +--------- app/Services/Social/TikTokAnalytics.php | 28 +---------- app/Services/Social/TikTokCreatorInfo.php | 28 +---------- app/Services/Social/TikTokPublisher.php | 28 +---------- app/Services/Social/TokenRefreshClient.php | 2 +- app/Services/Social/XAnalytics.php | 29 +----------- app/Services/Social/XPublisher.php | 26 +--------- app/Services/Social/YouTubeAnalytics.php | 30 +----------- app/Services/Social/YouTubePublisher.php | 28 +---------- 20 files changed, 29 insertions(+), 489 deletions(-) diff --git a/app/Services/Social/BlueskyPublisher.php b/app/Services/Social/BlueskyPublisher.php index 68c5fc15..bc7a5b65 100644 --- a/app/Services/Social/BlueskyPublisher.php +++ b/app/Services/Social/BlueskyPublisher.php @@ -6,7 +6,6 @@ use App\Enums\SocialAccount\Platform; use App\Exceptions\Social\BlueskyPublishException; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Media\MediaOptimizer; @@ -30,7 +29,7 @@ public function publish(PostPlatform $postPlatform): array // Refresh token if needed if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -268,50 +267,6 @@ private function buildPostUrl(string $handle, string $postId): string return "https://bsky.app/profile/{$handle}/post/{$postId}"; } - public function refreshToken(SocialAccount $account): void - { - $service = $account->meta['service'] ?? config('trypost.platforms.bluesky.default_service'); - $client = TokenRefreshClient::for(Platform::Bluesky); - - try { - $response = $client->send(fn () => $this->socialHttp()->withToken($account->refresh_token) - ->post("{$service}/xrpc/com.atproto.server.refreshSession")); - - $data = $response->json(); - $account->update([ - 'access_token' => data_get($data, 'accessJwt'), - 'refresh_token' => data_get($data, 'refreshJwt'), - 'token_expires_at' => now()->addHours(2), - ]); - - return; - } catch (TokenExpiredException) { - // refresh token rejected (4xx) — fall back to re-auth below - } - - if (isset($account->meta['password'])) { - try { - $reauth = $client->send(fn () => Http::post("{$service}/xrpc/com.atproto.server.createSession", [ - 'identifier' => $account->meta['identifier'], - 'password' => decrypt($account->meta['password']), - ])); - - $data = $reauth->json(); - $account->update([ - 'access_token' => data_get($data, 'accessJwt'), - 'refresh_token' => data_get($data, 'refreshJwt'), - 'token_expires_at' => now()->addHours(2), - ]); - - return; - } catch (TokenExpiredException) { - // re-auth rejected with stored credentials — fall through - } - } - - throw new TokenExpiredException('Bluesky session expired'); - } - private function handleApiError(Response $response): never { throw BlueskyPublishException::fromApiResponse($response); diff --git a/app/Services/Social/Concerns/HasSocialHttpClient.php b/app/Services/Social/Concerns/HasSocialHttpClient.php index 301032a2..1aa41c7c 100644 --- a/app/Services/Social/Concerns/HasSocialHttpClient.php +++ b/app/Services/Social/Concerns/HasSocialHttpClient.php @@ -5,9 +5,7 @@ namespace App\Services\Social\Concerns; use App\Models\PostPlatform; -use App\Models\SocialAccount; use Illuminate\Http\Client\PendingRequest; -use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; trait HasSocialHttpClient @@ -28,25 +26,6 @@ protected function validateContentLength(PostPlatform $postPlatform): void ); } - protected function refreshTokenWithLock(SocialAccount $account, callable $refreshFn): void - { - $lock = Cache::lock("token_refresh:{$account->id}", 30); - - if (! $lock->get()) { - // Another process is refreshing, wait and reload - sleep(2); - $account->refresh(); - - return; - } - - try { - $refreshFn(); - } finally { - $lock->release(); - } - } - protected function socialHttp(): PendingRequest { return Http::retry( diff --git a/app/Services/Social/ConnectionVerifier.php b/app/Services/Social/ConnectionVerifier.php index d13bb620..a0b25dfb 100644 --- a/app/Services/Social/ConnectionVerifier.php +++ b/app/Services/Social/ConnectionVerifier.php @@ -112,10 +112,10 @@ public function refreshToken(SocialAccount $account): void private function refreshLinkedInToken(SocialAccount $account): void { if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for LinkedIn account'); + throw new TokenExpiredException("No refresh token available for {$account->platform->label()} account"); } - $response = TokenRefreshClient::for(Platform::LinkedIn)->send(fn () => Http::asForm() + $response = TokenRefreshClient::for($account->platform)->send(fn () => Http::asForm() ->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, diff --git a/app/Services/Social/InstagramAnalytics.php b/app/Services/Social/InstagramAnalytics.php index 126a4626..de677310 100644 --- a/app/Services/Social/InstagramAnalytics.php +++ b/app/Services/Social/InstagramAnalytics.php @@ -5,15 +5,12 @@ namespace App\Services\Social; use App\Enums\PostPlatform\ContentType; -use App\Enums\SocialAccount\Platform; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Carbon\CarbonInterface; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class InstagramAnalytics @@ -48,7 +45,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array $this->baseUrl = $account->platform->instagramGraphBaseUrl(); if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -92,7 +89,7 @@ private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $si $this->baseUrl = $account->platform->instagramGraphBaseUrl(); if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -200,27 +197,4 @@ private function getHttpClient(): PendingRequest { return $this->socialHttp(); } - - private function refreshToken(SocialAccount $account): void - { - if ($account->platform === Platform::InstagramFacebook) { - return; - } - - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for Instagram account'); - } - - $response = TokenRefreshClient::for(Platform::Instagram)->send(fn () => Http::get(config('trypost.platforms.instagram.auth_api').'/refresh_access_token', [ - 'grant_type' => 'ig_refresh_token', - 'access_token' => $account->access_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - } } diff --git a/app/Services/Social/InstagramPublisher.php b/app/Services/Social/InstagramPublisher.php index 9ad27dd5..d4da8274 100644 --- a/app/Services/Social/InstagramPublisher.php +++ b/app/Services/Social/InstagramPublisher.php @@ -5,11 +5,9 @@ namespace App\Services\Social; use App\Enums\PostPlatform\ContentType; -use App\Enums\SocialAccount\Platform; use App\Exceptions\Social\ErrorCategory; use App\Exceptions\Social\InstagramPublishException; use App\Models\PostPlatform; -use App\Models\SocialAccount; use App\Services\Media\MediaOptimizer; use App\Services\Social\Concerns\HasSocialHttpClient; use Illuminate\Http\Client\Response; @@ -32,7 +30,7 @@ public function publish(PostPlatform $postPlatform): array $this->baseUrl = $account->platform->instagramGraphBaseUrl(); if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -395,29 +393,6 @@ private function waitForMediaProcessing(string $containerId, string $accessToken Log::warning('Instagram media processing timeout, proceeding anyway'); } - private function refreshToken(SocialAccount $account): void - { - // Instagram via Facebook uses page tokens that don't expire - if ($account->platform === Platform::InstagramFacebook) { - return; - } - - $response = TokenRefreshClient::for(Platform::Instagram)->send(fn () => Http::get(config('trypost.platforms.instagram.auth_api').'/refresh_access_token', [ - 'grant_type' => 'ig_refresh_token', - 'access_token' => $account->access_token, - ])); - - $data = $response->json(); - $newToken = data_get($data, 'access_token'); - - $account->update([ - 'access_token' => $newToken, - 'refresh_token' => $newToken, - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - - } - private function handleApiError(Response $response): never { throw InstagramPublishException::fromApiResponse($response); diff --git a/app/Services/Social/LinkedInPageAnalytics.php b/app/Services/Social/LinkedInPageAnalytics.php index 98c1d519..3fdba549 100644 --- a/app/Services/Social/LinkedInPageAnalytics.php +++ b/app/Services/Social/LinkedInPageAnalytics.php @@ -4,15 +4,12 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Carbon\CarbonInterface; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class LinkedInPageAnalytics @@ -54,7 +51,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array } if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -84,7 +81,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $since, CarbonInterface $until): array { if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -234,27 +231,4 @@ private function getHttpClient(): PendingRequest 'X-Restli-Protocol-Version' => '2.0.0', ]); } - - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for LinkedIn Page account'); - } - - $response = TokenRefreshClient::for(Platform::LinkedInPage)->send(fn () => Http::asForm() - ->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - 'client_id' => config('services.linkedin-openid.client_id'), - 'client_secret' => config('services.linkedin-openid.client_secret'), - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - } } diff --git a/app/Services/Social/LinkedInPagePublisher.php b/app/Services/Social/LinkedInPagePublisher.php index 7486f5d3..8269a21b 100644 --- a/app/Services/Social/LinkedInPagePublisher.php +++ b/app/Services/Social/LinkedInPagePublisher.php @@ -46,7 +46,7 @@ public function publish(PostPlatform $postPlatform): array $this->hasRetried = false; if ($this->account->is_token_expired || $this->account->is_token_expiring_soon) { - $this->refreshTokenWithLock($this->account, fn () => $this->refreshToken($this->account)); + app(ConnectionVerifier::class)->refreshToken($this->account); $this->account->refresh(); } @@ -81,7 +81,7 @@ private function retryWithRefresh(PostPlatform $postPlatform, ?string $content, $this->hasRetried = true; try { - $this->refreshToken($this->account); + app(ConnectionVerifier::class)->refreshToken($this->account); $this->account->refresh(); $this->accessToken = $this->account->access_token; @@ -452,32 +452,6 @@ private function waitForVideoProcessing(string $videoUrn, int $maxAttempts = 30) Log::warning('LinkedIn Page video processing timeout, proceeding anyway'); } - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for LinkedIn Page account'); - } - - $response = TokenRefreshClient::for(Platform::LinkedInPage)->send(fn () => Http::asForm() - ->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - 'client_id' => config('services.linkedin-openid.client_id'), - 'client_secret' => config('services.linkedin-openid.client_secret'), - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - - // Sync tokens to LinkedIn personal if it exists - app(LinkedInTokenSynchronizer::class)->syncTokens($account); - } - private function handleApiError(Response $response): never { throw LinkedInPublishException::fromApiResponse($response); diff --git a/app/Services/Social/LinkedInPublisher.php b/app/Services/Social/LinkedInPublisher.php index ed6d230c..567bf1e0 100644 --- a/app/Services/Social/LinkedInPublisher.php +++ b/app/Services/Social/LinkedInPublisher.php @@ -46,7 +46,7 @@ public function publish(PostPlatform $postPlatform): array $this->hasRetried = false; if ($this->account->is_token_expired || $this->account->is_token_expiring_soon) { - $this->refreshTokenWithLock($this->account, fn () => $this->refreshToken($this->account)); + app(ConnectionVerifier::class)->refreshToken($this->account); $this->account->refresh(); } @@ -75,7 +75,7 @@ private function retryWithRefresh(PostPlatform $postPlatform, ?string $content, $this->hasRetried = true; try { - $this->refreshToken($this->account); + app(ConnectionVerifier::class)->refreshToken($this->account); $this->account->refresh(); $this->accessToken = $this->account->access_token; @@ -433,32 +433,6 @@ private function waitForVideoProcessing(string $videoUrn, int $maxAttempts = 30) Log::warning('LinkedIn video processing timeout, proceeding anyway'); } - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for LinkedIn account'); - } - - $response = TokenRefreshClient::for(Platform::LinkedIn)->send(fn () => Http::asForm() - ->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - 'client_id' => config('services.linkedin.client_id'), - 'client_secret' => config('services.linkedin.client_secret'), - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - - // Sync tokens to LinkedIn Page if it exists - app(LinkedInTokenSynchronizer::class)->syncTokens($account); - } - private function handleApiError(Response $response): never { throw LinkedInPublishException::fromApiResponse($response); diff --git a/app/Services/Social/PinterestAnalytics.php b/app/Services/Social/PinterestAnalytics.php index 2a343b9e..d18391a4 100644 --- a/app/Services/Social/PinterestAnalytics.php +++ b/app/Services/Social/PinterestAnalytics.php @@ -4,15 +4,12 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Carbon\CarbonInterface; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class PinterestAnalytics @@ -50,7 +47,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array } if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -95,7 +92,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $since, CarbonInterface $until): array { if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -161,27 +158,4 @@ private function getHttpClient(): PendingRequest { return $this->socialHttp()->withToken($this->accessToken); } - - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for Pinterest account'); - } - - $response = TokenRefreshClient::for(Platform::Pinterest)->send(fn () => Http::withBasicAuth( - config('services.pinterest.client_id'), - config('services.pinterest.client_secret'), - )->asForm()->post(config('trypost.platforms.pinterest.api').'/oauth/token', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - } } diff --git a/app/Services/Social/PinterestPublisher.php b/app/Services/Social/PinterestPublisher.php index 116b93f5..5fe1d7b3 100644 --- a/app/Services/Social/PinterestPublisher.php +++ b/app/Services/Social/PinterestPublisher.php @@ -34,7 +34,7 @@ public function publish(PostPlatform $postPlatform): array $account = $postPlatform->socialAccount; if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -400,34 +400,13 @@ private function waitForMediaProcessing(SocialAccount $account, string $mediaId, ); } - private function refreshToken(SocialAccount $account): void - { - $response = TokenRefreshClient::for(Platform::Pinterest)->send(fn () => Http::asForm() - ->withBasicAuth( - config('services.pinterest.client_id'), - config('services.pinterest.client_secret') - ) - ->post($this->baseUrl.'/oauth/token', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : now()->addDays(30), - ]); - } - /** * Get user's boards for board selection. */ public function getBoards(SocialAccount $account): array { if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } diff --git a/app/Services/Social/ThreadsAnalytics.php b/app/Services/Social/ThreadsAnalytics.php index dc245f63..26258979 100644 --- a/app/Services/Social/ThreadsAnalytics.php +++ b/app/Services/Social/ThreadsAnalytics.php @@ -4,14 +4,12 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Carbon\CarbonInterface; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class ThreadsAnalytics @@ -49,7 +47,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array } if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -81,7 +79,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $since, CarbonInterface $until): array { if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -138,19 +136,4 @@ private function getHttpClient(): PendingRequest { return $this->socialHttp(); } - - private function refreshToken(SocialAccount $account): void - { - $response = TokenRefreshClient::for(Platform::Threads)->send(fn () => Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [ - 'grant_type' => 'th_refresh_token', - 'access_token' => $account->access_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - } } diff --git a/app/Services/Social/ThreadsPublisher.php b/app/Services/Social/ThreadsPublisher.php index 135f9dd4..28d3d19a 100644 --- a/app/Services/Social/ThreadsPublisher.php +++ b/app/Services/Social/ThreadsPublisher.php @@ -4,13 +4,10 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; use App\Exceptions\Social\ThreadsPublishException; use App\Models\PostPlatform; -use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Illuminate\Http\Client\Response; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class ThreadsPublisher @@ -33,7 +30,7 @@ public function publish(PostPlatform $postPlatform): array $account = $postPlatform->socialAccount; if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -303,25 +300,6 @@ private function waitForMediaProcessing(string $containerId, string $accessToken throw new \Exception('Threads media processing timeout after '.$maxAttempts.' attempts'); } - private function refreshToken(SocialAccount $account): void - { - // Threads uses long-lived tokens that can be refreshed - $response = TokenRefreshClient::for(Platform::Threads)->send(fn () => Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [ - 'grant_type' => 'th_refresh_token', - 'access_token' => $account->access_token, - ])); - - $data = $response->json(); - - $newToken = data_get($data, 'access_token'); - - $account->update([ - 'access_token' => $newToken, - 'refresh_token' => $newToken, - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - } - private function handleApiError(Response $response): never { throw ThreadsPublishException::fromApiResponse($response); diff --git a/app/Services/Social/TikTokAnalytics.php b/app/Services/Social/TikTokAnalytics.php index 7532ae3c..a1a80d63 100644 --- a/app/Services/Social/TikTokAnalytics.php +++ b/app/Services/Social/TikTokAnalytics.php @@ -4,13 +4,10 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; -use App\Exceptions\TokenExpiredException; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class TikTokAnalytics @@ -39,7 +36,7 @@ public function getMetrics(SocialAccount $account): array private function fetchMetricsFromApi(SocialAccount $account): array { if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -160,27 +157,4 @@ private function getHttpClient(): PendingRequest { return $this->socialHttp()->asJson()->withToken($this->accessToken); } - - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for TikTok account'); - } - - $response = TokenRefreshClient::for(Platform::TikTok)->send(fn () => Http::asForm() - ->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [ - 'client_key' => config('services.tiktok.client_id'), - 'client_secret' => config('services.tiktok.client_secret'), - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - } } diff --git a/app/Services/Social/TikTokCreatorInfo.php b/app/Services/Social/TikTokCreatorInfo.php index d3fe8ab5..418316bd 100644 --- a/app/Services/Social/TikTokCreatorInfo.php +++ b/app/Services/Social/TikTokCreatorInfo.php @@ -4,13 +4,10 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; -use App\Exceptions\TokenExpiredException; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class TikTokCreatorInfo @@ -62,7 +59,7 @@ public function fetch(SocialAccount $account): array private function fetchFresh(SocialAccount $account): array { if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -124,27 +121,4 @@ private function getHttpClient(): PendingRequest { return $this->socialHttp()->asJson()->withToken($this->accessToken); } - - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for TikTok account'); - } - - $response = TokenRefreshClient::for(Platform::TikTok)->send(fn () => Http::asForm() - ->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [ - 'client_key' => config('services.tiktok.client_id'), - 'client_secret' => config('services.tiktok.client_secret'), - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - } } diff --git a/app/Services/Social/TikTokPublisher.php b/app/Services/Social/TikTokPublisher.php index c0b875d0..554e3402 100644 --- a/app/Services/Social/TikTokPublisher.php +++ b/app/Services/Social/TikTokPublisher.php @@ -7,13 +7,11 @@ use App\Enums\SocialAccount\Platform; use App\Exceptions\Social\ErrorCategory; use App\Exceptions\Social\TikTokPublishException; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Illuminate\Http\Client\PendingRequest; use Illuminate\Http\Client\Response; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class TikTokPublisher @@ -38,7 +36,7 @@ public function publish(PostPlatform $postPlatform): array $account = $postPlatform->socialAccount; if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -317,30 +315,6 @@ private function buildTikTokUrl(SocialAccount $account, ?string $postId = null): return null; } - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for TikTok account'); - } - - $response = TokenRefreshClient::for(Platform::TikTok)->send(fn () => Http::asForm() - ->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [ - 'client_key' => config('services.tiktok.client_id'), - 'client_secret' => config('services.tiktok.client_secret'), - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - - } - private function handleApiError(Response $response): never { throw TikTokPublishException::fromApiResponse($response); diff --git a/app/Services/Social/TokenRefreshClient.php b/app/Services/Social/TokenRefreshClient.php index 05b1c774..861389f7 100644 --- a/app/Services/Social/TokenRefreshClient.php +++ b/app/Services/Social/TokenRefreshClient.php @@ -49,7 +49,7 @@ public function send(Closure $request): Response throw new PlatformUnavailableException("{$name} API unreachable: {$e->getMessage()}"); } - if ($response->serverError()) { + if ($response->serverError() || $response->status() === 429) { throw new PlatformUnavailableException( "{$name} API returned {$response->status()} during token refresh", $response->status(), diff --git a/app/Services/Social/XAnalytics.php b/app/Services/Social/XAnalytics.php index 4d620260..1b22c7e9 100644 --- a/app/Services/Social/XAnalytics.php +++ b/app/Services/Social/XAnalytics.php @@ -4,8 +4,6 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; @@ -49,7 +47,7 @@ public function getMetrics(SocialAccount $account, ?CarbonInterface $since = nul private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $since, CarbonInterface $until): array { if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -165,7 +163,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array } if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -200,27 +198,4 @@ private function getHttpClient(): PendingRequest { return $this->socialHttp()->withToken($this->accessToken); } - - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for X account'); - } - - $response = TokenRefreshClient::for(Platform::X)->send(fn () => $this->socialHttp() - ->withBasicAuth(config('services.x.client_id'), config('services.x.client_secret')) - ->asForm() - ->post(config('trypost.platforms.x.api').'/oauth2/token', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - } } diff --git a/app/Services/Social/XPublisher.php b/app/Services/Social/XPublisher.php index 56f1a10a..fefdda53 100644 --- a/app/Services/Social/XPublisher.php +++ b/app/Services/Social/XPublisher.php @@ -6,9 +6,7 @@ use App\Enums\SocialAccount\Platform; use App\Exceptions\Social\XPublishException; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; -use App\Models\SocialAccount; use App\Services\Media\MediaOptimizer; use App\Services\Social\Concerns\HasSocialHttpClient; use Illuminate\Http\Client\PendingRequest; @@ -39,7 +37,7 @@ public function publish(PostPlatform $postPlatform): array // Refresh token if expired or expiring soon if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -329,28 +327,6 @@ private function waitForProcessing(string $mediaId, int $maxAttempts = 20): bool return false; } - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for X account'); - } - - $response = TokenRefreshClient::for(Platform::X)->send(fn () => Http::asForm() - ->withBasicAuth(config('services.x.client_id'), config('services.x.client_secret')) - ->post("{$this->baseUrl}/oauth2/token", [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => now()->addSeconds(data_get($data, 'expires_in', 7200)), - ]); - } - private function handleApiError(Response $response): never { throw XPublishException::fromApiResponse($response); diff --git a/app/Services/Social/YouTubeAnalytics.php b/app/Services/Social/YouTubeAnalytics.php index f082667b..43d01248 100644 --- a/app/Services/Social/YouTubeAnalytics.php +++ b/app/Services/Social/YouTubeAnalytics.php @@ -4,15 +4,12 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Carbon\CarbonInterface; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class YouTubeAnalytics @@ -50,7 +47,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array } if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -102,7 +99,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $since, CarbonInterface $until): array { if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -162,27 +159,4 @@ private function getHttpClient(): PendingRequest { return $this->socialHttp()->withToken($this->accessToken); } - - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for YouTube account'); - } - - $response = TokenRefreshClient::for(Platform::YouTube)->send(fn () => Http::asForm() - ->post(config('trypost.platforms.youtube.oauth_api').'/token', [ - 'client_id' => config('services.google.client_id'), - 'client_secret' => config('services.google.client_secret'), - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - } } diff --git a/app/Services/Social/YouTubePublisher.php b/app/Services/Social/YouTubePublisher.php index 3840766d..b3a68fd6 100644 --- a/app/Services/Social/YouTubePublisher.php +++ b/app/Services/Social/YouTubePublisher.php @@ -4,10 +4,8 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; use App\Exceptions\Social\ErrorCategory; use App\Exceptions\Social\YouTubePublishException; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; @@ -36,7 +34,7 @@ public function publish(PostPlatform $postPlatform): array $account = $postPlatform->socialAccount; if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -224,30 +222,6 @@ private function buildTitle(string $content): string return $title.$shortsTag; } - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for YouTube account'); - } - - $response = TokenRefreshClient::for(Platform::YouTube)->send(fn () => Http::asForm() - ->post(config('trypost.platforms.youtube.oauth_api').'/token', [ - 'client_id' => config('services.google.client_id'), - 'client_secret' => config('services.google.client_secret'), - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - - } - private function handleGoogleError(Exception $e): never { throw YouTubePublishException::fromGoogleException($e); From cf80e1fcaec925f5ea54c4a7e75ec4864254c994 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 19 May 2026 09:21:43 -0300 Subject: [PATCH 07/14] refactor(social): single source of truth for token redaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same shape of bug as the refreshToken duplication: the regex that strips access_token / Bearer headers from logged HTTP bodies existed in three near-identical copies (HasSocialHttpClient trait, TokenRefreshClient, SocialPublishException), drifting subtly — SocialPublishException was missing the JSON "token" pattern. Extracts a TokenRedactor::redact(string) helper and routes all callers through it. Adding a new token format now means one regex in one file. --- .../Social/SocialPublishException.php | 25 ++------------ .../Social/Concerns/HasSocialHttpClient.php | 17 ++-------- app/Services/Social/TokenRedactor.php | 33 +++++++++++++++++++ app/Services/Social/TokenRefreshClient.php | 21 +----------- 4 files changed, 38 insertions(+), 58 deletions(-) create mode 100644 app/Services/Social/TokenRedactor.php diff --git a/app/Exceptions/Social/SocialPublishException.php b/app/Exceptions/Social/SocialPublishException.php index 384695e6..8d007728 100644 --- a/app/Exceptions/Social/SocialPublishException.php +++ b/app/Exceptions/Social/SocialPublishException.php @@ -4,6 +4,7 @@ namespace App\Exceptions\Social; +use App\Services\Social\TokenRedactor; use RuntimeException; abstract class SocialPublishException extends RuntimeException @@ -27,32 +28,10 @@ public function context(): array 'category' => $this->category->value, 'platform_error_code' => $this->platformErrorCode, 'user_message' => $this->userMessage, - 'raw_response' => $this->redactTokens($this->rawResponse), + 'raw_response' => $this->rawResponse !== null ? TokenRedactor::redact($this->rawResponse) : null, ]; } - private function redactTokens(?string $text): ?string - { - if ($text === null) { - return null; - } - - // Redact common token patterns from API error responses - return preg_replace( - [ - '/access_token=([^&"\s]+)/', - '/"access_token"\s*:\s*"([^"]+)"/', - '/Bearer\s+\S+/', - ], - [ - 'access_token=[REDACTED]', - '"access_token":"[REDACTED]"', - 'Bearer [REDACTED]', - ], - $text - ); - } - abstract public static function fromApiResponse(mixed $response): static; abstract public function platform(): string; diff --git a/app/Services/Social/Concerns/HasSocialHttpClient.php b/app/Services/Social/Concerns/HasSocialHttpClient.php index 1aa41c7c..092ca717 100644 --- a/app/Services/Social/Concerns/HasSocialHttpClient.php +++ b/app/Services/Social/Concerns/HasSocialHttpClient.php @@ -5,6 +5,7 @@ namespace App\Services\Social\Concerns; use App\Models\PostPlatform; +use App\Services\Social\TokenRedactor; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Http; @@ -38,20 +39,6 @@ protected function socialHttp(): PendingRequest protected function redactResponseBody(string $body): string { - return preg_replace( - [ - '/access_token=([^&"\s]+)/', - '/"access_token"\s*:\s*"([^"]+)"/', - '/Bearer\s+\S+/', - '/"token"\s*:\s*"([^"]+)"/', - ], - [ - 'access_token=[REDACTED]', - '"access_token":"[REDACTED]"', - 'Bearer [REDACTED]', - '"token":"[REDACTED]"', - ], - $body - ); + return TokenRedactor::redact($body); } } diff --git a/app/Services/Social/TokenRedactor.php b/app/Services/Social/TokenRedactor.php new file mode 100644 index 00000000..bc154048 --- /dev/null +++ b/app/Services/Social/TokenRedactor.php @@ -0,0 +1,33 @@ +failed()) { Log::error("TokenRefreshClient: {$name} token refresh failed", [ - 'body' => $this->redactBody($response->body()), + 'body' => TokenRedactor::redact($response->body()), ]); $body = $response->json(); @@ -71,23 +71,4 @@ public function send(Closure $request): Response return $response; } - - private function redactBody(string $body): string - { - return preg_replace( - [ - '/access_token=([^&"\s]+)/', - '/"access_token"\s*:\s*"([^"]+)"/', - '/Bearer\s+\S+/', - '/"token"\s*:\s*"([^"]+)"/', - ], - [ - 'access_token=[REDACTED]', - '"access_token":"[REDACTED]"', - 'Bearer [REDACTED]', - '"token":"[REDACTED]"', - ], - $body - ); - } } From 123996bbc0c4e495042386a89a7d0461f52c2af8 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 19 May 2026 09:25:01 -0300 Subject: [PATCH 08/14] refactor(social): TokenRedactor::redact accepts nullable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lets the call sites drop the explicit null check ternary. The redactor itself owns the null handling — one less conditional at every caller. --- app/Exceptions/Social/SocialPublishException.php | 2 +- app/Services/Social/TokenRedactor.php | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/Exceptions/Social/SocialPublishException.php b/app/Exceptions/Social/SocialPublishException.php index 8d007728..2af28382 100644 --- a/app/Exceptions/Social/SocialPublishException.php +++ b/app/Exceptions/Social/SocialPublishException.php @@ -28,7 +28,7 @@ public function context(): array 'category' => $this->category->value, 'platform_error_code' => $this->platformErrorCode, 'user_message' => $this->userMessage, - 'raw_response' => $this->rawResponse !== null ? TokenRedactor::redact($this->rawResponse) : null, + 'raw_response' => TokenRedactor::redact($this->rawResponse), ]; } diff --git a/app/Services/Social/TokenRedactor.php b/app/Services/Social/TokenRedactor.php index bc154048..8da132e5 100644 --- a/app/Services/Social/TokenRedactor.php +++ b/app/Services/Social/TokenRedactor.php @@ -12,8 +12,12 @@ */ class TokenRedactor { - public static function redact(string $body): string + public static function redact(?string $body): ?string { + if ($body === null) { + return null; + } + return preg_replace( [ '/access_token=([^&"\s]+)/', From f975171a9a10630ecd3070f8579f1ab8fa3e59e9 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 19 May 2026 09:34:04 -0300 Subject: [PATCH 09/14] test(social): close coverage gaps for TokenRedactor and 429 handling - TokenRedactorTest (7 unit tests): all four regex patterns, multiple secrets in one body, null input, and the no-op pass-through case. Guards against silent regression of the redaction regexes (which previously drifted across three duplicated copies). - ConnectionVerifierTest: HTTP 429 during refresh raises PlatformUnavailableException, not TokenExpiredException. Locks in the rate-limit-as-transient behavior added when consolidating the refresh logic. --- .../Social/ConnectionVerifierTest.php | 15 ++++++ .../Services/Social/TokenRedactorTest.php | 47 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 tests/Unit/Services/Social/TokenRedactorTest.php diff --git a/tests/Feature/Services/Social/ConnectionVerifierTest.php b/tests/Feature/Services/Social/ConnectionVerifierTest.php index 8488a972..34c94f92 100644 --- a/tests/Feature/Services/Social/ConnectionVerifierTest.php +++ b/tests/Feature/Services/Social/ConnectionVerifierTest.php @@ -409,6 +409,21 @@ expect(fn () => $verifier->refreshToken($account))->toThrow(TokenExpiredException::class); }); +test('429 during refresh raises PlatformUnavailableException (rate limit is transient)', function () { + Http::fake([ + 'api.x.com/2/oauth2/token' => Http::response(['error' => 'rate_limit_exceeded'], 429), + ]); + + $account = SocialAccount::factory()->x()->create([ + 'token_expires_at' => now()->subMinutes(5), + 'refresh_token' => 'old_refresh_token', + ]); + + $verifier = new ConnectionVerifier; + + expect(fn () => $verifier->refreshToken($account))->toThrow(PlatformUnavailableException::class); +}); + test('bluesky 5xx during refresh raises PlatformUnavailable even when password fallback is stored', function () { Http::fake([ 'bsky.social/xrpc/com.atproto.server.refreshSession' => Http::response('upstream timeout', 503), diff --git a/tests/Unit/Services/Social/TokenRedactorTest.php b/tests/Unit/Services/Social/TokenRedactorTest.php new file mode 100644 index 00000000..f3e4df09 --- /dev/null +++ b/tests/Unit/Services/Social/TokenRedactorTest.php @@ -0,0 +1,47 @@ +toBe('POST https://api.example.com?access_token=[REDACTED]&page=1'); +}); + +test('redact strips access_token in JSON form', function () { + $input = '{"data":{"access_token":"abc123xyz","expires_in":3600}}'; + + expect(TokenRedactor::redact($input))->toBe('{"data":{"access_token":"[REDACTED]","expires_in":3600}}'); +}); + +test('redact strips Bearer authorization header', function () { + $input = "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.payload.sig\n"; + + expect(TokenRedactor::redact($input))->toBe("Authorization: Bearer [REDACTED]\n"); +}); + +test('redact strips "token" JSON field', function () { + $input = '{"token":"shhh-secret","other":"keep"}'; + + expect(TokenRedactor::redact($input))->toBe('{"token":"[REDACTED]","other":"keep"}'); +}); + +test('redact handles multiple secrets in the same body', function () { + $input = 'access_token=one&refresh_token=two with Bearer xyz'; + $output = TokenRedactor::redact($input); + + expect($output)->toContain('access_token=[REDACTED]') + ->toContain('Bearer [REDACTED]'); +}); + +test('redact returns null when input is null', function () { + expect(TokenRedactor::redact(null))->toBeNull(); +}); + +test('redact returns the input unchanged when nothing matches', function () { + $input = 'plain log line with no secrets'; + + expect(TokenRedactor::redact($input))->toBe($input); +}); From 04975020e417802f35fa8f80afef7be5b0af1ea4 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 19 May 2026 09:37:16 -0300 Subject: [PATCH 10/14] test(social): tests pull OAuth URLs from config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same rule applied to production code in earlier commits now applies to tests: Http::fake patterns and assertions read from config('trypost.platforms.*.oauth_api' / '.api' / '.default_service') instead of hardcoded strings. Hardcoded URLs in tests drift silently when the config changes. Also documents the rule in CLAUDE.md under "External Service URLs" so new code (and tests) start in the right place — only the host comes from config, path/RPC segments stay inline next to the call. --- CLAUDE.md | 7 +++++++ .../Social/ConnectionVerifierTest.php | 20 +++++++++++-------- .../Social/LinkedInPageAnalyticsTest.php | 12 +++++------ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 651d0c6f..f8e47ba8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -302,6 +302,13 @@ ## String Interpolation - Use curly braces `{}` even for simple variables to keep the boundary explicit and to allow object/array access without ambiguity. - Single quotes are still preferred when the string has no interpolation. +## External Service URLs + +- NEVER hardcode third-party API hosts, OAuth endpoints, or per-platform service URLs (e.g. `https://api.x.com/2`, `https://www.linkedin.com/oauth/v2/accessToken`, `https://bsky.social`). They live in `config/trypost.php` under `platforms.` with a matching `env(...)` default, so self-hosted users can override them and we have a single source of truth. + - Production code: `config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken'`, never the literal URL. + - Tests: use the same `config(...)` value in `Http::fake([...])` — `Http::fake([config('trypost.platforms.x.api').'/oauth2/token' => ...])`. Tests with hardcoded URLs drift silently when the config changes. + - Path/route segments after the host (e.g. `/oauth/v2/accessToken`, `/xrpc/com.atproto.server.refreshSession`) are part of the provider's protocol spec — those stay inline next to the call. Only the host comes from config. + ## TryPost.it Documentation - All our documentation to final user it's under https://docs.trypost.it diff --git a/tests/Feature/Services/Social/ConnectionVerifierTest.php b/tests/Feature/Services/Social/ConnectionVerifierTest.php index 34c94f92..4b16294d 100644 --- a/tests/Feature/Services/Social/ConnectionVerifierTest.php +++ b/tests/Feature/Services/Social/ConnectionVerifierTest.php @@ -364,14 +364,16 @@ }); test('5xx during refresh raises PlatformUnavailableException, not TokenExpiredException', function () { + $service = config('trypost.platforms.bluesky.default_service'); + Http::fake([ - 'bsky.social/xrpc/com.atproto.server.refreshSession' => Http::response('upstream timeout', 503), + "{$service}/xrpc/com.atproto.server.refreshSession" => Http::response('upstream timeout', 503), ]); $account = SocialAccount::factory()->bluesky()->create([ 'token_expires_at' => now()->subMinutes(5), 'refresh_token' => 'old_refresh_token', - 'meta' => ['service' => 'https://bsky.social'], + 'meta' => ['service' => $service], ]); $verifier = new ConnectionVerifier; @@ -381,7 +383,7 @@ test('connection failure during refresh raises PlatformUnavailableException', function () { Http::fake([ - 'oauth2.googleapis.com/token' => fn () => throw new ConnectionException('cURL error 7: connection refused'), + config('trypost.platforms.youtube.oauth_api').'/token' => fn () => throw new ConnectionException('cURL error 7: connection refused'), ]); $account = SocialAccount::factory()->youtube()->create([ @@ -396,7 +398,7 @@ test('4xx during refresh keeps raising TokenExpiredException', function () { Http::fake([ - 'api.x.com/2/oauth2/token' => Http::response(['error' => 'invalid_grant'], 400), + config('trypost.platforms.x.api').'/oauth2/token' => Http::response(['error' => 'invalid_grant'], 400), ]); $account = SocialAccount::factory()->x()->create([ @@ -411,7 +413,7 @@ test('429 during refresh raises PlatformUnavailableException (rate limit is transient)', function () { Http::fake([ - 'api.x.com/2/oauth2/token' => Http::response(['error' => 'rate_limit_exceeded'], 429), + config('trypost.platforms.x.api').'/oauth2/token' => Http::response(['error' => 'rate_limit_exceeded'], 429), ]); $account = SocialAccount::factory()->x()->create([ @@ -425,16 +427,18 @@ }); test('bluesky 5xx during refresh raises PlatformUnavailable even when password fallback is stored', function () { + $service = config('trypost.platforms.bluesky.default_service'); + Http::fake([ - 'bsky.social/xrpc/com.atproto.server.refreshSession' => Http::response('upstream timeout', 503), - 'bsky.social/xrpc/com.atproto.server.createSession' => Http::response('upstream timeout', 503), + "{$service}/xrpc/com.atproto.server.refreshSession" => Http::response('upstream timeout', 503), + "{$service}/xrpc/com.atproto.server.createSession" => Http::response('upstream timeout', 503), ]); $account = SocialAccount::factory()->bluesky()->create([ 'token_expires_at' => now()->subMinutes(5), 'refresh_token' => 'old_refresh_token', 'meta' => [ - 'service' => 'https://bsky.social', + 'service' => $service, 'identifier' => 'user.bsky.social', 'password' => encrypt('app-password'), ], diff --git a/tests/Feature/Services/Social/LinkedInPageAnalyticsTest.php b/tests/Feature/Services/Social/LinkedInPageAnalyticsTest.php index 0836940d..6805fd4d 100644 --- a/tests/Feature/Services/Social/LinkedInPageAnalyticsTest.php +++ b/tests/Feature/Services/Social/LinkedInPageAnalyticsTest.php @@ -34,13 +34,16 @@ }); test('linkedin page analytics refresh hits the configured oauth host', function () { + $oauthApi = config('trypost.platforms.linkedin.oauth_api'); + $api = config('trypost.platforms.linkedin-page.api'); + Http::fake([ - 'www.linkedin.com/oauth/v2/accessToken' => Http::response([ + "{$oauthApi}/oauth/v2/accessToken" => Http::response([ 'access_token' => 'new_token', 'refresh_token' => 'new_refresh_token', 'expires_in' => 5184000, ], 200), - 'api.linkedin.com/rest/socialActions/*' => Http::response([ + "{$api}/rest/socialActions/*" => Http::response([ 'likesSummary' => ['totalLikes' => 0], 'commentsSummary' => ['aggregatedTotalComments' => 0], ], 200), @@ -48,8 +51,5 @@ (new LinkedInPageAnalytics)->fetchPostMetrics($this->postPlatform); - Http::assertSent(fn ($request) => str_contains( - $request->url(), - rtrim((string) config('trypost.platforms.linkedin.oauth_api'), '/').'/oauth/v2/accessToken' - )); + Http::assertSent(fn ($request) => str_contains($request->url(), "{$oauthApi}/oauth/v2/accessToken")); }); From a42962c0cabdce3cd492b6f48113f9de0576d151 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 19 May 2026 09:47:12 -0300 Subject: [PATCH 11/14] fix(social): publish retry honors PlatformUnavailable too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Edge case from the prior commits: if the publisher throws TokenExpired (401 path), PublishToSocialPlatform attempts refreshAccountToken() to recover. That internally goes through ConnectionVerifier::verify, which can now raise PlatformUnavailable (5xx). The old catch (\Throwable) swallowed it but the loop still fell through to markAsTokenExpired — meaning a transient platform outage during a retry could still flip the account to expired. Adds an explicit PlatformUnavailable catch in the retry block: marks the post failed with category platform_unavailable and breaks before touching the account status. --- app/Jobs/PublishToSocialPlatform.php | 13 ++++++++ .../Jobs/PublishToSocialPlatformTest.php | 30 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/app/Jobs/PublishToSocialPlatform.php b/app/Jobs/PublishToSocialPlatform.php index 0ee2b719..b37c40d1 100644 --- a/app/Jobs/PublishToSocialPlatform.php +++ b/app/Jobs/PublishToSocialPlatform.php @@ -130,6 +130,19 @@ public function handle(): void $this->refreshAccountToken(); continue; + } catch (PlatformUnavailableException $refreshError) { + Log::warning('Publish skipped: platform unavailable during retry refresh', [ + 'post_platform_id' => $this->postPlatform->id, + 'platform' => $this->postPlatform->platform->value, + 'error' => $refreshError->getMessage(), + ]); + + $this->postPlatform->markAsFailed($refreshError->getMessage(), [ + 'category' => 'platform_unavailable', + 'http_status' => $refreshError->httpStatus, + 'failed_at' => now()->toIso8601String(), + ]); + break; } catch (\Throwable $refreshError) { Log::error('Token refresh failed during publish retry', [ 'post_platform_id' => $this->postPlatform->id, diff --git a/tests/Feature/Jobs/PublishToSocialPlatformTest.php b/tests/Feature/Jobs/PublishToSocialPlatformTest.php index 30d4c175..ecedf7af 100644 --- a/tests/Feature/Jobs/PublishToSocialPlatformTest.php +++ b/tests/Feature/Jobs/PublishToSocialPlatformTest.php @@ -110,6 +110,36 @@ expect($this->socialAccount->status)->toBe(AccountStatus::TokenExpired); }); +test('publish does NOT mark account expired when retry refresh hits platform unavailable', function () { + Event::fake(); + Mail::fake(); + + // Publisher first throws TokenExpired (401-style), the retry-refresh + // path goes through ConnectionVerifier::verify which can in turn raise + // PlatformUnavailable if the platform is down. The account must stay + // Connected — it was the platform that failed, not the token. + $publisher = Mockery::mock(LinkedInPublisher::class); + $publisher->shouldReceive('publish')->andThrow(new TokenExpiredException('Token expired', '401')); + + $verifier = Mockery::mock(ConnectionVerifier::class); + $verifier->shouldReceive('verify')->andThrow( + new PlatformUnavailableException('LinkedIn API returned 503 during token refresh', 503) + ); + + $this->app->instance(LinkedInPublisher::class, $publisher); + $this->app->instance(ConnectionVerifier::class, $verifier); + + (new PublishToSocialPlatform($this->postPlatform))->handle(); + + $this->postPlatform->refresh(); + $this->socialAccount->refresh(); + + expect($this->postPlatform->status)->toBe(PlatformStatus::Failed); + expect($this->postPlatform->error_context['category'] ?? null)->toBe('platform_unavailable'); + expect($this->postPlatform->error_context['http_status'] ?? null)->toBe(503); + expect($this->socialAccount->status)->toBe(AccountStatus::Connected); +}); + test('publish to social platform does NOT mark account expired when platform is unavailable', function () { Event::fake(); Mail::fake(); From 6c6078fa6a4aa0336275fa6761171939629a46d9 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 19 May 2026 09:52:49 -0300 Subject: [PATCH 12/14] refactor(social): drop redundant \$account->refresh() after CV refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every refresh*Token method inside ConnectionVerifier already finishes with \$account->update(...) (mutates the model) + \$account->refresh() (reloads from DB to pick up sibling updates like LinkedInTokenSynchronizer). The lock-contention branch in refreshToken() also calls \$account->refresh() before returning. So a second \$account->refresh() in the caller was always a redundant SELECT — no scenario where it actually pulled a different value than what CV already left in memory. Removed from all 15 call sites, plus fixed the duplicated "Mastodon tokens don't expire" comment. --- app/Services/Social/BlueskyPublisher.php | 1 - app/Services/Social/ConnectionVerifier.php | 5 ++--- app/Services/Social/InstagramAnalytics.php | 2 -- app/Services/Social/InstagramPublisher.php | 1 - app/Services/Social/LinkedInPageAnalytics.php | 2 -- app/Services/Social/LinkedInPagePublisher.php | 2 -- app/Services/Social/LinkedInPublisher.php | 2 -- app/Services/Social/PinterestAnalytics.php | 2 -- app/Services/Social/PinterestPublisher.php | 2 -- app/Services/Social/ThreadsAnalytics.php | 2 -- app/Services/Social/ThreadsPublisher.php | 1 - app/Services/Social/TikTokAnalytics.php | 1 - app/Services/Social/TikTokCreatorInfo.php | 1 - app/Services/Social/TikTokPublisher.php | 1 - app/Services/Social/XAnalytics.php | 2 -- app/Services/Social/XPublisher.php | 1 - app/Services/Social/YouTubeAnalytics.php | 2 -- app/Services/Social/YouTubePublisher.php | 1 - 18 files changed, 2 insertions(+), 29 deletions(-) diff --git a/app/Services/Social/BlueskyPublisher.php b/app/Services/Social/BlueskyPublisher.php index bc7a5b65..38094fc6 100644 --- a/app/Services/Social/BlueskyPublisher.php +++ b/app/Services/Social/BlueskyPublisher.php @@ -30,7 +30,6 @@ public function publish(PostPlatform $postPlatform): array // Refresh token if needed if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $medias = $postPlatform->post->mediaItems; diff --git a/app/Services/Social/ConnectionVerifier.php b/app/Services/Social/ConnectionVerifier.php index a0b25dfb..7f25319f 100644 --- a/app/Services/Social/ConnectionVerifier.php +++ b/app/Services/Social/ConnectionVerifier.php @@ -99,9 +99,8 @@ public function refreshToken(SocialAccount $account): void Platform::Pinterest => $this->refreshPinterestToken($account), Platform::Threads => $this->refreshThreadsToken($account), Platform::Instagram => $this->refreshInstagramToken($account), - // InstagramFacebook uses page tokens that don't expire (like Facebook) - // Mastodon tokens don't expire - // Mastodon tokens don't expire + // Facebook / InstagramFacebook use Page tokens that don't expire. + // Mastodon tokens don't expire either. default => null, }; } finally { diff --git a/app/Services/Social/InstagramAnalytics.php b/app/Services/Social/InstagramAnalytics.php index de677310..50c20c88 100644 --- a/app/Services/Social/InstagramAnalytics.php +++ b/app/Services/Social/InstagramAnalytics.php @@ -46,7 +46,6 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $this->accessToken = $account->access_token; @@ -90,7 +89,6 @@ private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $si if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $this->accessToken = $account->access_token; diff --git a/app/Services/Social/InstagramPublisher.php b/app/Services/Social/InstagramPublisher.php index d4da8274..c22bef71 100644 --- a/app/Services/Social/InstagramPublisher.php +++ b/app/Services/Social/InstagramPublisher.php @@ -31,7 +31,6 @@ public function publish(PostPlatform $postPlatform): array if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $instagramId = $account->platform_user_id; diff --git a/app/Services/Social/LinkedInPageAnalytics.php b/app/Services/Social/LinkedInPageAnalytics.php index 3fdba549..71fb4355 100644 --- a/app/Services/Social/LinkedInPageAnalytics.php +++ b/app/Services/Social/LinkedInPageAnalytics.php @@ -52,7 +52,6 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } // platform_post_id is the share URN (e.g., "urn:li:share:12345"). @@ -82,7 +81,6 @@ private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $si { if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $this->accessToken = $account->access_token; diff --git a/app/Services/Social/LinkedInPagePublisher.php b/app/Services/Social/LinkedInPagePublisher.php index 8269a21b..c1a186f6 100644 --- a/app/Services/Social/LinkedInPagePublisher.php +++ b/app/Services/Social/LinkedInPagePublisher.php @@ -47,7 +47,6 @@ public function publish(PostPlatform $postPlatform): array if ($this->account->is_token_expired || $this->account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($this->account); - $this->account->refresh(); } $this->accessToken = $this->account->access_token; @@ -82,7 +81,6 @@ private function retryWithRefresh(PostPlatform $postPlatform, ?string $content, try { app(ConnectionVerifier::class)->refreshToken($this->account); - $this->account->refresh(); $this->accessToken = $this->account->access_token; $organizationId = $this->account->meta['organization_id'] ?? null; diff --git a/app/Services/Social/LinkedInPublisher.php b/app/Services/Social/LinkedInPublisher.php index 567bf1e0..8053d458 100644 --- a/app/Services/Social/LinkedInPublisher.php +++ b/app/Services/Social/LinkedInPublisher.php @@ -47,7 +47,6 @@ public function publish(PostPlatform $postPlatform): array if ($this->account->is_token_expired || $this->account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($this->account); - $this->account->refresh(); } $this->accessToken = $this->account->access_token; @@ -76,7 +75,6 @@ private function retryWithRefresh(PostPlatform $postPlatform, ?string $content, try { app(ConnectionVerifier::class)->refreshToken($this->account); - $this->account->refresh(); $this->accessToken = $this->account->access_token; $personUrn = "urn:li:person:{$this->account->platform_user_id}"; diff --git a/app/Services/Social/PinterestAnalytics.php b/app/Services/Social/PinterestAnalytics.php index d18391a4..082963a2 100644 --- a/app/Services/Social/PinterestAnalytics.php +++ b/app/Services/Social/PinterestAnalytics.php @@ -48,7 +48,6 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $start = now()->subDays(90)->format('Y-m-d'); @@ -93,7 +92,6 @@ private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $si { if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $this->accessToken = $account->access_token; diff --git a/app/Services/Social/PinterestPublisher.php b/app/Services/Social/PinterestPublisher.php index 5fe1d7b3..994199fa 100644 --- a/app/Services/Social/PinterestPublisher.php +++ b/app/Services/Social/PinterestPublisher.php @@ -35,7 +35,6 @@ public function publish(PostPlatform $postPlatform): array if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $content = $postPlatform->post->content ? app(ContentSanitizer::class)->sanitize($postPlatform->post->content, $postPlatform->platform) : null; @@ -407,7 +406,6 @@ public function getBoards(SocialAccount $account): array { if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $response = $this->socialHttp()->withToken($account->access_token) diff --git a/app/Services/Social/ThreadsAnalytics.php b/app/Services/Social/ThreadsAnalytics.php index 26258979..ef196477 100644 --- a/app/Services/Social/ThreadsAnalytics.php +++ b/app/Services/Social/ThreadsAnalytics.php @@ -48,7 +48,6 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $response = $this->socialHttp() @@ -80,7 +79,6 @@ private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $si { if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $this->accessToken = $account->access_token; diff --git a/app/Services/Social/ThreadsPublisher.php b/app/Services/Social/ThreadsPublisher.php index 28d3d19a..c37402f1 100644 --- a/app/Services/Social/ThreadsPublisher.php +++ b/app/Services/Social/ThreadsPublisher.php @@ -31,7 +31,6 @@ public function publish(PostPlatform $postPlatform): array if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $userId = $account->platform_user_id; diff --git a/app/Services/Social/TikTokAnalytics.php b/app/Services/Social/TikTokAnalytics.php index a1a80d63..37861978 100644 --- a/app/Services/Social/TikTokAnalytics.php +++ b/app/Services/Social/TikTokAnalytics.php @@ -37,7 +37,6 @@ private function fetchMetricsFromApi(SocialAccount $account): array { if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $this->accessToken = $account->access_token; diff --git a/app/Services/Social/TikTokCreatorInfo.php b/app/Services/Social/TikTokCreatorInfo.php index 418316bd..7571d067 100644 --- a/app/Services/Social/TikTokCreatorInfo.php +++ b/app/Services/Social/TikTokCreatorInfo.php @@ -60,7 +60,6 @@ private function fetchFresh(SocialAccount $account): array { if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $this->accessToken = $account->access_token; diff --git a/app/Services/Social/TikTokPublisher.php b/app/Services/Social/TikTokPublisher.php index 554e3402..d26a8a8a 100644 --- a/app/Services/Social/TikTokPublisher.php +++ b/app/Services/Social/TikTokPublisher.php @@ -37,7 +37,6 @@ public function publish(PostPlatform $postPlatform): array if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $this->accessToken = $account->access_token; diff --git a/app/Services/Social/XAnalytics.php b/app/Services/Social/XAnalytics.php index 1b22c7e9..c562b2a8 100644 --- a/app/Services/Social/XAnalytics.php +++ b/app/Services/Social/XAnalytics.php @@ -48,7 +48,6 @@ private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $si { if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $this->accessToken = $account->access_token; @@ -164,7 +163,6 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $this->accessToken = $account->access_token; diff --git a/app/Services/Social/XPublisher.php b/app/Services/Social/XPublisher.php index fefdda53..625a840b 100644 --- a/app/Services/Social/XPublisher.php +++ b/app/Services/Social/XPublisher.php @@ -38,7 +38,6 @@ public function publish(PostPlatform $postPlatform): array // Refresh token if expired or expiring soon if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $this->accessToken = $account->access_token; diff --git a/app/Services/Social/YouTubeAnalytics.php b/app/Services/Social/YouTubeAnalytics.php index 43d01248..c4eb8342 100644 --- a/app/Services/Social/YouTubeAnalytics.php +++ b/app/Services/Social/YouTubeAnalytics.php @@ -48,7 +48,6 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $this->accessToken = $account->access_token; @@ -100,7 +99,6 @@ private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $si { if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $this->accessToken = $account->access_token; diff --git a/app/Services/Social/YouTubePublisher.php b/app/Services/Social/YouTubePublisher.php index b3a68fd6..5b4467c5 100644 --- a/app/Services/Social/YouTubePublisher.php +++ b/app/Services/Social/YouTubePublisher.php @@ -35,7 +35,6 @@ public function publish(PostPlatform $postPlatform): array if ($account->is_token_expired || $account->is_token_expiring_soon) { app(ConnectionVerifier::class)->refreshToken($account); - $account->refresh(); } $media = $postPlatform->post->mediaItems; From d336f790594f54b148df7f3d2197fb61fec4e3d7 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 19 May 2026 10:03:30 -0300 Subject: [PATCH 13/14] feat(social): reschedule publish on PlatformUnavailable instead of failing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before: a scheduled post hitting a platform outage was marked Failed — user had to manually retry. Now the job reschedules itself for 10 minutes later and the PostPlatform shows status "Retrying". Loops indefinitely until the platform accepts the post. - Adds PostPlatformStatus::Retrying (existing string column, no migration) - PublishToSocialPlatform: PlatformUnavailable catch now calls rescheduleForRetry() which (a) updates the row to Retrying with retry_count + next_attempt_at in error_context, and (b) dispatches itself with a 10-minute delay. updatePostStatus() naturally leaves the parent Post in Publishing because Retrying is neither Published nor Failed. - Same treatment for the retry-refresh edge case (publisher throws TokenExpired, refresh subsequently fails with PlatformUnavailable). - i18n + frontend status config updated (en, pt-BR, es) for both posts.status.retrying and posts.edit.status.retrying. - Tests: 3 new tests covering the dispatched job, the edge case path, and retry_count increment across attempts. --- app/Enums/PostPlatform/Status.php | 1 + app/Jobs/PublishToSocialPlatform.php | 52 +++++++++------- lang/en/posts.php | 2 + lang/es/posts.php | 2 + lang/php_en.json | 2 +- lang/php_es.json | 2 +- lang/php_pt-BR.json | 2 +- lang/pt-BR/posts.php | 2 + resources/js/composables/usePostStatus.ts | 2 + .../Jobs/PublishToSocialPlatformTest.php | 59 ++++++++++++++----- 10 files changed, 87 insertions(+), 39 deletions(-) diff --git a/app/Enums/PostPlatform/Status.php b/app/Enums/PostPlatform/Status.php index 96501e52..354c683b 100644 --- a/app/Enums/PostPlatform/Status.php +++ b/app/Enums/PostPlatform/Status.php @@ -8,6 +8,7 @@ enum Status: string { case Pending = 'pending'; case Publishing = 'publishing'; + case Retrying = 'retrying'; case Published = 'published'; case Failed = 'failed'; } diff --git a/app/Jobs/PublishToSocialPlatform.php b/app/Jobs/PublishToSocialPlatform.php index b37c40d1..c040f6f0 100644 --- a/app/Jobs/PublishToSocialPlatform.php +++ b/app/Jobs/PublishToSocialPlatform.php @@ -112,17 +112,7 @@ public function handle(): void $this->postPlatform->markAsPublished(data_get($result, 'id'), data_get($result, 'url')); break; } catch (PlatformUnavailableException $e) { - Log::warning('Publish skipped: platform unavailable', [ - 'post_platform_id' => $this->postPlatform->id, - 'platform' => $this->postPlatform->platform->value, - 'error' => $e->getMessage(), - ]); - - $this->postPlatform->markAsFailed($e->getMessage(), [ - 'category' => 'platform_unavailable', - 'http_status' => $e->httpStatus, - 'failed_at' => now()->toIso8601String(), - ]); + $this->rescheduleForRetry($e); break; } catch (TokenExpiredException $e) { if ($attempt < $maxAttempts) { @@ -131,17 +121,7 @@ public function handle(): void continue; } catch (PlatformUnavailableException $refreshError) { - Log::warning('Publish skipped: platform unavailable during retry refresh', [ - 'post_platform_id' => $this->postPlatform->id, - 'platform' => $this->postPlatform->platform->value, - 'error' => $refreshError->getMessage(), - ]); - - $this->postPlatform->markAsFailed($refreshError->getMessage(), [ - 'category' => 'platform_unavailable', - 'http_status' => $refreshError->httpStatus, - 'failed_at' => now()->toIso8601String(), - ]); + $this->rescheduleForRetry($refreshError); break; } catch (\Throwable $refreshError) { Log::error('Token refresh failed during publish retry', [ @@ -208,6 +188,34 @@ private function refreshAccountToken(): void app(ConnectionVerifier::class)->verify($account); } + private function rescheduleForRetry(PlatformUnavailableException $e): void + { + $retryCount = (int) ($this->postPlatform->error_context['retry_count'] ?? 0) + 1; + $nextAttemptAt = now()->addMinutes(10); + + Log::warning('Publish rescheduled: platform unavailable', [ + 'post_platform_id' => $this->postPlatform->id, + 'platform' => $this->postPlatform->platform->value, + 'retry_count' => $retryCount, + 'next_attempt_at' => $nextAttemptAt->toIso8601String(), + 'error' => $e->getMessage(), + ]); + + $this->postPlatform->update([ + 'status' => PostPlatformStatus::Retrying, + 'error_message' => $e->getMessage(), + 'error_context' => [ + 'category' => 'platform_unavailable', + 'http_status' => $e->httpStatus, + 'retry_count' => $retryCount, + 'last_attempt_at' => now()->toIso8601String(), + 'next_attempt_at' => $nextAttemptAt->toIso8601String(), + ], + ]); + + self::dispatch($this->postPlatform)->delay($nextAttemptAt); + } + private function broadcastStatus(): void { PostPlatformStatusUpdated::dispatch($this->postPlatform->fresh()); diff --git a/lang/en/posts.php b/lang/en/posts.php index ffa00259..573d0192 100644 --- a/lang/en/posts.php +++ b/lang/en/posts.php @@ -175,6 +175,7 @@ 'draft' => 'Draft', 'scheduled' => 'Scheduled', 'publishing' => 'Publishing', + 'retrying' => 'Retrying', 'published' => 'Published', 'partially_published' => 'Partially Published', 'failed' => 'Failed', @@ -323,6 +324,7 @@ 'scheduled' => 'Scheduled', 'published' => 'Published', 'publishing' => 'Publishing...', + 'retrying' => 'Retrying...', 'failed' => 'Failed', ], diff --git a/lang/es/posts.php b/lang/es/posts.php index 7da4d67a..42cadbcb 100644 --- a/lang/es/posts.php +++ b/lang/es/posts.php @@ -175,6 +175,7 @@ 'draft' => 'Borrador', 'scheduled' => 'Programado', 'publishing' => 'Publicando', + 'retrying' => 'Reintentando', 'published' => 'Publicado', 'partially_published' => 'Parcialmente publicado', 'failed' => 'Fallido', @@ -323,6 +324,7 @@ 'scheduled' => 'Programado', 'published' => 'Publicado', 'publishing' => 'Publicando...', + 'retrying' => 'Reintentando...', 'failed' => 'Fallido', ], diff --git a/lang/php_en.json b/lang/php_en.json index adf01b91..981c6c15 100644 --- a/lang/php_en.json +++ b/lang/php_en.json @@ -1 +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.connect_cta":"Connect","accounts.no_accounts":"No accounts connected yet","accounts.no_accounts_description":"Connect your social networks to start scheduling and publishing posts","accounts.no_search_results":"No accounts match your search","accounts.try_different_search":"Try a different keyword or clear the search.","accounts.search":"Search accounts...","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.table.account":"Account","accounts.table.platform":"Platform","accounts.table.status":"Status","accounts.table.last_used":"Last used","accounts.table.added":"Added","accounts.table.active":"Active","accounts.never_used":"Never used","accounts.status.connected":"Connected","accounts.status.disconnected":"Disconnected","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.","accounts.popup_callback.title_success":"Connected","accounts.popup_callback.title_error":"Error","accounts.popup_callback.closing":"This window will close automatically...","accounts.popup_callback.close_now":"You can close this window now.","accounts.popup_callback.connected":"Account connected!","accounts.popup_callback.reconnected":"Account reconnected!","accounts.popup_callback.error_connecting":"Error connecting account. Please try again.","accounts.popup_callback.error_connecting_page":"Error connecting page. Please try again.","accounts.popup_callback.error_connecting_channel":"Error connecting channel. Please try again.","accounts.popup_callback.session_expired":"Session expired. Please try again.","accounts.popup_callback.workspace_not_found":"Workspace not found.","accounts.popup_callback.invalid_state":"Invalid state. Please try again.","accounts.popup_callback.failed_to_authenticate":"Failed to authenticate.","accounts.popup_callback.failed_to_get_profile":"Failed to get profile.","accounts.popup_callback.page_not_found":"Page not found.","accounts.popup_callback.channel_not_found":"Channel not found.","accounts.popup_callback.no_facebook_pages":"No Facebook Pages found. You need to be an admin of at least one page.","accounts.popup_callback.no_facebook_instagram_pages":"No Facebook Pages with linked Instagram accounts found.","accounts.popup_callback.no_youtube_channels":"No YouTube channels found. Please create a channel first.","accounts.popup_callback.not_linkedin_admin":"You are not an administrator of any LinkedIn page.","analytics.no_accounts":"No connected accounts with analytics.","analytics.no_accounts_match":"No accounts match.","analytics.search_account":"Search account…","analytics.select_account":"Select an account to view analytics.","analytics.no_data":"No analytics data available.","analytics.metrics.avg_view_duration":"Avg. View Duration (s)","analytics.metrics.avg_view_percentage":"Avg. View Percentage","analytics.metrics.bookmarks":"Bookmarks","analytics.metrics.clicks":"Clicks","analytics.metrics.comments":"Comments","analytics.metrics.engagement":"Engagement","analytics.metrics.favourites":"Favourites","analytics.metrics.followers":"Followers","analytics.metrics.following":"Following","analytics.metrics.impressions":"Impressions","analytics.metrics.interactions":"Interactions","analytics.metrics.likes":"Likes","analytics.metrics.minutes_watched":"Minutes Watched","analytics.metrics.organic_followers":"Organic Followers","analytics.metrics.outbound_clicks":"Outbound Clicks","analytics.metrics.page_followers":"Page Followers","analytics.metrics.page_reach":"Page Reach","analytics.metrics.page_views":"Page Views","analytics.metrics.paid_followers":"Paid Followers","analytics.metrics.pin_click_rate":"Pin Click Rate","analytics.metrics.pin_clicks":"Pin Clicks","analytics.metrics.posts_engagement":"Posts Engagement","analytics.metrics.posts_reach":"Posts Reach","analytics.metrics.quotes":"Quotes","analytics.metrics.reach":"Reach","analytics.metrics.reblogs":"Reblogs","analytics.metrics.recent_comments":"Recent Comments","analytics.metrics.recent_likes":"Recent Likes","analytics.metrics.recent_shares":"Recent Shares","analytics.metrics.replies":"Replies","analytics.metrics.reposts":"Reposts","analytics.metrics.retweets":"Retweets","analytics.metrics.saves":"Saves","analytics.metrics.shares":"Shares","analytics.metrics.subscribers_gained":"Subscribers Gained","analytics.metrics.subscribers_lost":"Subscribers Lost","analytics.metrics.total_likes":"Total Likes","analytics.metrics.video_views":"Video Views","analytics.metrics.videos":"Videos","analytics.metrics.views":"Views","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.add_to_post":"Add to post","assets.search_placeholder":"Search media...","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","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.signatures.title":"Signatures","auth.slides.signatures.description":"Save reusable signatures (hashtags, links, signoffs) and append them to posts with one click.","auth.or_continue_with":"Or continue with","auth.google_login":"Log in with Google","auth.google_signup":"Sign up with Google","auth.github_login":"Log in with GitHub","auth.github_signup":"Sign up with GitHub","auth.github_email_unavailable":"Unable to retrieve your email from GitHub. Make your GitHub email public or grant the email scope, then try again.","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.upgrade_dialog.title":"Upgrade your plan","billing.upgrade_dialog.description":"Pick a plan that fits your needs.","billing.upgrade_dialog.current_plan":"Current plan","billing.upgrade_dialog.current_short":"Current","billing.upgrade_dialog.current_badge":"Current","billing.upgrade_dialog.subscribe":"Subscribe","billing.upgrade_dialog.switch":"Switch to this plan","billing.upgrade_dialog.switch_short":"Switch","billing.upgrade_dialog.switch_to_yearly":"Switch to yearly","billing.upgrade_dialog.switch_to_monthly":"Switch to monthly","billing.upgrade_dialog.unavailable":"Unavailable","billing.upgrade_dialog.reasons.workspace_limit":"You've reached the workspace limit on your current plan. Upgrade to create more workspaces.","billing.upgrade_dialog.reasons.social_account_limit":"You've reached the social account limit on your current plan. Upgrade to connect more accounts.","billing.upgrade_dialog.reasons.member_limit":"You've reached the team member limit on your current plan. Upgrade to invite more people.","billing.subscribe.page_title":"Choose your plan","billing.subscribe.eyebrow":"Pricing","billing.subscribe.title":"Choose the right plan for you","billing.subscribe.description":"Pick the plan that fits you. Billed monthly or annually.","billing.subscribe.monthly":"Monthly","billing.subscribe.yearly":"Yearly","billing.subscribe.per_month":"monthly","billing.subscribe.per_year":"yearly","billing.subscribe.billed_monthly":"Billed monthly","billing.subscribe.billed_yearly":"Billed annually","billing.subscribe.features_included":"What's included:","billing.subscribe.everything_in":"Everything in :plan, plus:","billing.subscribe.save_months":"2 months free","billing.subscribe.popular":"Most popular","billing.subscribe.subscribe_cta":"Subscribe","billing.subscribe.prices.starter.monthly":"$19","billing.subscribe.prices.starter.yearly_per_month":"$16","billing.subscribe.prices.starter.yearly":"$190","billing.subscribe.prices.plus.monthly":"$29","billing.subscribe.prices.plus.yearly_per_month":"$24","billing.subscribe.prices.plus.yearly":"$290","billing.subscribe.prices.pro.monthly":"$49","billing.subscribe.prices.pro.yearly_per_month":"$41","billing.subscribe.prices.pro.yearly":"$490","billing.subscribe.prices.max.monthly":"$99","billing.subscribe.prices.max.yearly_per_month":"$83","billing.subscribe.prices.max.yearly":"$990","billing.subscribe.features.social_accounts":":count social accounts","billing.subscribe.features.workspaces":":count workspaces","billing.subscribe.features.members":":count team members","billing.subscribe.features.credits":":count AI credits/mo","billing.subscribe.credit_tooltips.starter":"Roughly 150 medium-length posts plus 5 AI images per month.","billing.subscribe.credit_tooltips.plus":"Roughly 300 medium-length posts plus 10 AI images per month.","billing.subscribe.credit_tooltips.pro":"Roughly 700 medium-length posts plus 30 AI images per month.","billing.subscribe.credit_tooltips.max":"Roughly 2,000 medium-length posts plus 100 AI images per month.","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.no_payment_method":"No payment method on file yet.","billing.subscription.expires_on":"Expires :month/:year","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.flash.plan_changed":"You are now on the :plan plan.","billing.flash.cannot_manage":"Only the account owner can manage billing.","billing.flash.cannot_downgrade.workspaces":"Cannot switch to :plan: you have :count workspaces but the plan only allows :limit.","billing.flash.cannot_downgrade.social_accounts":"Cannot switch to :plan: you have :count social accounts but the plan only allows :limit.","billing.flash.cannot_downgrade.members":"Cannot switch to :plan: you have :count team members (including invites) but the plan only allows :limit.","billing.flash.credits_exhausted":"Out of AI credits — your monthly :limit allowance has been used. Upgrade your plan or wait until next month.","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","comments.today":"Today","comments.yesterday":"Yesterday","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.confirm_modal.delete_keyword":"delete","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.date_range_picker.placeholder":"Pick a date range","common.date_range_picker.today":"Today","common.date_range_picker.yesterday":"Yesterday","common.date_range_picker.last_7_days":"Last 7 days","common.date_range_picker.last_30_days":"Last 30 days","common.date_range_picker.last_3_months":"Last 3 months","common.date_range_picker.last_6_months":"Last 6 months","common.date_range_picker.last_12_months":"Last 12 months","common.date_range_picker.this_month":"This month","common.date_range_picker.last_month":"Last month","common.date_range_picker.year_to_date":"Year to date","common.date_range_picker.last_year":"Last year","common.cancel":"Cancel","common.clear":"Clear","common.close":"Close","common.loading_more":"Loading more...","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.no_search_results":"No labels match your search","labels.try_different_search":"Try a different keyword or clear the search.","labels.create_first_label":"Create your first label","labels.table.name":"Name","labels.table.created_at":"Created","labels.actions.edit":"Edit label","labels.actions.delete":"Delete 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.mentioned.subject":":name mentioned you on TryPost","mail.mentioned.title":":name mentioned you","mail.mentioned.intro":":name mentioned you in a post comment.","mail.mentioned.cta":"View comment","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","notifications.post_ready.title":"Your post is ready","notifications.post_ready.body":"The AI just finished. Tap to review and publish.","notifications.account_disconnected.title":":platform account disconnected","notifications.account_disconnected.body":":account needs to be reconnected","notifications.account_token_expired.title":":platform account needs to be reconnected","notifications.account_token_expired.body":":account session expired — please reconnect to keep posting","posts.title":"Posts","posts.search":"Search posts...","posts.all_posts":"All Posts","posts.new_post":"New Post","posts.no_posts":"No posts found","posts.no_search_results":"No posts match your search","posts.try_different_search":"Try a different keyword or clear the search.","posts.start_creating":"Start by creating your first post.","posts.filter_by_label":"Filter by label","posts.label_search_placeholder":"Search labels...","posts.no_labels":"No labels found.","posts.clear_label_filter":"Clear label filter","posts.table.post":"Post","posts.table.status":"Status","posts.table.content":"Content","posts.table.platforms":"Platforms","posts.table.labels":"Labels","posts.table.scheduled_at":"Date","posts.table.actions":"","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","posts.actions.duplicate":"Duplicate","posts.actions.copy_id":"Copy ID","posts.actions.copied":"ID copied to clipboard","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.content_exceeds_platform":":platform: too long by :over chars (max :limit).","posts.form.tiktok.settings":"TikTok Settings","posts.form.tiktok.variant_label":"Post type","posts.form.tiktok.variant.video":"Video","posts.form.tiktok.variant.photo":"Photo carousel","posts.form.tiktok.posting_to":"Posting to","posts.form.tiktok.privacy_level":"Who can see this video?","posts.form.tiktok.privacy_placeholder":"Select visibility","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.private_disabled_branded":"Branded content visibility cannot be set to private.","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.disclose":"Disclose video content","posts.form.tiktok.disclose_hint":"Turn on to disclose that this video promotes goods or services in exchange for something of value. Your video could promote yourself, a third party, or both.","posts.form.tiktok.promotional_organic_title":"Your photo/video will be labeled as \"Promotional content\".","posts.form.tiktok.promotional_paid_title":"Your photo/video will be labeled as \"Paid partnership\".","posts.form.tiktok.promotional_description":"This cannot be changed once your video is posted.","posts.form.tiktok.compliance_incomplete":"You need to indicate if your content promotes yourself, a third party, or both.","posts.form.tiktok.privacy_required":"TikTok privacy level is required when publishing.","posts.form.tiktok.branded_cleared_private":"Privacy was cleared because Branded Content cannot be private.","posts.form.tiktok.interaction_disabled_by_creator":"Disabled by your TikTok account settings.","posts.form.tiktok.max_duration_exceeded":"Video is :duration s long but this account can only post videos up to :max s.","posts.form.tiktok.processing_hint":"After publishing, it may take a few minutes for the content to process and appear on your TikTok profile.","posts.form.tiktok.brand_organic":"Your brand","posts.form.tiktok.brand_organic_hint":"You are promoting yourself or your own brand. This video will be classified as Brand Organic.","posts.form.tiktok.brand_content":"Branded content","posts.form.tiktok.brand_content_hint":"You are promoting another brand or a third party. This video will be classified as Branded Content.","posts.form.tiktok.compliance.agree":"By posting, you agree to TikTok's","posts.form.tiktok.compliance.music_usage":"Music Usage Confirmation","posts.form.tiktok.compliance.and":"and","posts.form.tiktok.compliance.branded_policy":"Branded Content Policy","posts.form.instagram.settings":"Instagram Settings","posts.form.instagram.posting_to":"Posting to","posts.form.instagram.variant_label":"Post type","posts.form.instagram.variant.feed":"Feed Post","posts.form.instagram.variant.reel":"Reel","posts.form.instagram.variant.story":"Story","posts.form.instagram.aspect_label":"Aspect ratio","posts.form.instagram.aspect.square":"Square (1:1)","posts.form.instagram.aspect.portrait":"Portrait (4:5)","posts.form.instagram.aspect.landscape":"Landscape (16:9)","posts.form.instagram.aspect.original":"Original","posts.form.facebook.settings":"Facebook Settings","posts.form.facebook.posting_to":"Posting to","posts.form.facebook.variant_label":"Post type","posts.form.facebook.variant.post":"Post","posts.form.facebook.variant.reel":"Reel","posts.form.facebook.variant.story":"Story","posts.form.linkedin.settings":"LinkedIn Settings","posts.form.linkedin.settings_page":"LinkedIn Page Settings","posts.form.linkedin.posting_to":"Posting to","posts.form.linkedin.variant_label":"Post type","posts.form.linkedin.variant.post":"Post","posts.form.linkedin.variant.carousel":"Carousel","posts.form.pinterest.settings":"Pinterest Settings","posts.form.pinterest.posting_to":"Posting to","posts.form.pinterest.variant_label":"Pin type","posts.form.pinterest.variant.pin":"Pin","posts.form.pinterest.variant.video_pin":"Video Pin","posts.form.pinterest.variant.carousel":"Carousel","posts.form.pinterest.board":"Board","posts.form.pinterest.select_board":"Select a board","posts.form.pinterest.no_boards":"No Pinterest boards found. Create one in your Pinterest account first.","posts.form.pinterest.search_board":"Search boards...","posts.form.pinterest.no_board_found":"No board matches your search.","posts.form.pinterest.board_required":"Select a Pinterest board to publish this post.","posts.form.warnings.no_variant":"Pick a post type to continue.","posts.form.warnings.requires_media":"This post type requires at least one image or video.","posts.form.warnings.max_files_exceeded":"This post type accepts up to :max media files (you have :current).","posts.form.warnings.min_files_required":"This post type requires at least :min media files (you have :current).","posts.form.warnings.no_video_allowed":"This post type does not accept videos.","posts.form.warnings.no_image_allowed":"This post type accepts only videos.","posts.form.warnings.gif_not_allowed":"This platform does not accept GIF. Remove the GIF or choose a different network.","posts.form.warnings.image_too_large":"Image exceeds the :max limit for this post type (yours is :current).","posts.form.warnings.video_too_large":"Video exceeds the :max limit for this post type (yours is :current).","posts.form.warnings.video_too_long":"Video is :current long, but this post type allows up to :max.","posts.form.warnings.aspect_ratio_too_narrow":"Aspect ratio :current is too tall for this post type (min :min).","posts.form.warnings.aspect_ratio_too_wide":"Aspect ratio :current is too wide for this post type (max :max).","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.ai.generate.button_tooltip":"Generate with AI","posts.ai.generate.title":"Generate post with AI","posts.ai.generate.description":"Describe what the post should be about. The AI will use your brand context to write it.","posts.ai.generate.prompt_label":"What is this post about?","posts.ai.generate.prompt_placeholder":"e.g. Announce our new image-generation feature for carousels","posts.ai.generate.preview_label":"Preview","posts.ai.generate.start":"Generate","posts.ai.generate.apply":"Use this content","posts.ai.generate.retry":"Try again","posts.ai.generate.cancel":"Cancel","posts.ai.review.button_tooltip":"Review with AI","posts.ai.review.title":"Review post with AI","posts.ai.review.description":"AI scans for grammar, spelling, and clarity. Apply the suggestions you agree with.","posts.ai.review.loading":"Reviewing your text...","posts.ai.review.no_issues":"No issues found. Looks good.","posts.ai.review.original":"Original","posts.ai.review.suggestion":"Suggestion","posts.ai.review.apply":"Apply","posts.ai.review.apply_all":"Apply all","posts.ai.review.applied":"Applied","posts.ai.review.cancel":"Cancel","posts.show.title":"Post Details","posts.show.edit":"Edit","posts.show.back":"Back","posts.show.no_content":"No caption","posts.show.platforms":"Platforms","posts.show.no_platforms":"No platforms selected.","posts.show.view_on_platform":"View on platform","posts.show.published_on":"Published on :date","posts.show.scheduled_for":"Scheduled for :date","posts.show.draft":"Draft","posts.show.status_pending":"Pending","posts.show.metrics":"Metrics","posts.show.metrics_loading":"Loading metrics…","posts.show.metrics_unavailable":"Metrics unavailable for this platform yet.","posts.show.metrics_empty":"No metrics returned.","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.schedule_date":"Schedule date","posts.edit.unschedule":"Unschedule","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.preview_empty.title":"No platform selected","posts.edit.preview_empty.description":"Select a platform to publish to see the preview.","posts.edit.drop_zone_title":"Add media","posts.edit.drop_zone_subtitle":"Drag & drop files or click to browse","posts.edit.add":"Add","posts.edit.publish_to":"Publish to","posts.edit.organize":"Organize","posts.edit.signatures":"Signatures","posts.edit.view_on_platform":"View on platform","posts.edit.platform_status":"Platform status","posts.edit.compliance_incomplete":"Some platform settings are incomplete or incompatible with the attached media.","posts.edit.compliance.requires_media":"Add an image or video to publish here.","posts.edit.compliance.too_many_files":"Only :max file(s) allowed for this format.","posts.edit.compliance.too_few_files":"Add at least :min files for this format.","posts.edit.compliance.no_videos":"Only images are allowed for this format.","posts.edit.compliance.no_images":"Only videos are allowed for this format.","posts.edit.compliance.no_gifs":"GIFs are not supported here.","posts.edit.compliance.video_too_large":"Video exceeds the size limit for this platform.","posts.edit.compliance.video_too_long":"Video must be under :seconds seconds for this format.","posts.edit.compliance.image_too_large":"Image exceeds the size limit for this platform.","posts.edit.compliance.aspect_ratio_invalid":"Aspect ratio is not supported by this format.","posts.edit.compliance.no_content_type":"Pick a content type for this platform.","posts.edit.publishing":"Publishing...","posts.edit.publishing_overlay_title":"Your post is being published","posts.edit.publishing_overlay_subtitle":"This can take a few moments. You can safely leave this page.","posts.edit.scheduled_overlay_title":"This post is scheduled","posts.edit.scheduled_overlay_subtitle":"Scheduled for :date. Unschedule it first to make changes.","posts.edit.unschedule_cta":"Unschedule to edit","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.media_picker.title":"Pick from gallery","posts.edit.media_picker.search":"Search media...","posts.edit.media_picker.empty":"No media in your gallery yet","posts.edit.media_picker.cancel":"Cancel","posts.edit.media_picker.add":"Add","posts.edit.media_picker.add_count":"Add :count","posts.edit.emoji_picker.search":"Search emoji","posts.edit.emoji_picker.empty":"No emojis found","posts.edit.emoji_picker.recent":"Frequently used","posts.edit.emoji_picker.smileys":"Smileys & emotion","posts.edit.emoji_picker.people":"People & body","posts.edit.emoji_picker.nature":"Animals & nature","posts.edit.emoji_picker.food":"Food & drink","posts.edit.emoji_picker.activities":"Activities","posts.edit.emoji_picker.travel":"Travel & places","posts.edit.emoji_picker.objects":"Objects","posts.edit.emoji_picker.symbols":"Symbols","posts.edit.emoji_picker.flags":"Flags","posts.edit.status.scheduled":"Scheduled","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.signatures_modal.search":"Search signatures...","posts.edit.signatures_modal.no_results":"No signatures 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.tiktok_photo.label":"Photo carousel","posts.content_types.tiktok_photo.description":"Up to 35 photos as a swipeable carousel","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":"Standard image pin","posts.content_types.pinterest_video_pin.label":"Video Pin","posts.content_types.pinterest_video_pin.description":"Video pin (4s - 15min)","posts.content_types.pinterest_carousel.label":"Carousel","posts.content_types.pinterest_carousel.description":"Multi-image carousel (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.deleted":"Post deleted successfully!","posts.flash.duplicated":"Post duplicated as a draft.","posts.flash.cannot_edit_published":"Published posts cannot be edited.","posts.flash.cannot_delete_published":"Published posts cannot be deleted.","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","posts.errors.account_token_expired":"Social account session expired — please reconnect","posts.delete.title":"Delete post?","posts.delete.description":"This action can't be undone. The post and all its media will be permanently removed.","posts.delete.confirm":"Yes, delete","posts.delete.cancel":"Cancel","posts.create.title":"Create a new post","posts.create.description":"Choose how you want to start.","posts.create.scratch_title":"Start from scratch","posts.create.scratch_description":"Open a blank post and write everything yourself.","posts.create.ai_title":"Generate with AI","posts.create.ai_description":"Describe what you want and AI generates the content for you.","posts.create.ai_configure_description":"Pick a format and describe the post you want to create.","posts.create.template_title":"Use a template","posts.create.template_description":"Pick from our curated templates and customize.","posts.create.coming_soon":"Coming soon","posts.create.preview.image_title":"Image title","posts.create.preview.image_body":"Image body","posts.create.steps.format_title":"Choose a format","posts.create.steps.format_description":"Select the type of post you want to create.","posts.create.steps.account_title":"Choose an account","posts.create.steps.account_description":"Select the social account to publish to.","posts.create.steps.media_title":"Media options","posts.create.steps.media_carousel":"How many slides?","posts.create.steps.media_optional":"Include images?","posts.create.steps.media_optional_label":"How many images?","posts.create.steps.media_none":"None","posts.create.steps.media_count_label":"Number of images","posts.create.steps.prompt_title":"Describe your post","posts.create.steps.prompt_label":"What is this post about?","posts.create.steps.prompt_placeholder":"e.g. Announce our new carousel feature for Instagram","posts.create.steps.preview_error":"Something went wrong. Please try again.","posts.create.steps.loading_page_title":"Generating your post","posts.create.steps.loading_eta":"Estimated time: about :minutes.","posts.create.steps.loading_eta_minute_one":"1 minute","posts.create.steps.loading_eta_minute_other":":count minutes","posts.create.steps.loading_leave_title":"You can keep working.","posts.create.steps.loading_leave_body":"We will notify you when the post is ready.","posts.create.steps.loading_leave_cta":"Go to calendar","posts.create.steps.loading_create_another_cta":"Create another post","posts.create.steps.loading_tip_credits":"Each AI image uses about 15 credits.","posts.create.steps.loading_tip_edit":"You will be able to edit everything once the post is ready.","posts.create.steps.loading_tip_draft":"Generated posts land in your drafts.","posts.create.steps.loading_tip_brand":"Tweak your brand settings to influence future posts.","posts.create.steps.loading_tip_carousel":"Carousels deliver one slide per uploaded image.","posts.create.steps.loading_tip_quality":"Image quality is set to balance speed and cost.","posts.create.steps.create":"Create post","posts.create.steps.back":"Back","posts.create.steps.next":"Continue","posts.create.steps.cancel":"Cancel","posts.create.steps.discard":"Discard","posts.create.steps.retry":"Try again","posts.create.steps.no_platforms":"No connected accounts","posts.create.steps.connect_first":"Connect at least one social account to use AI generation.","posts.create.steps.format.instagram_feed":"Instagram Feed Post","posts.create.steps.format.instagram_carousel":"Instagram Carousel","posts.create.steps.format.linkedin_post":"LinkedIn Post","posts.create.steps.format.linkedin_page_post":"LinkedIn Page Post","posts.create.steps.format.x_post":"X Post","posts.create.steps.format.bluesky_post":"Bluesky Post","posts.create.steps.format.threads_post":"Threads Post","posts.create.steps.format.mastodon_post":"Mastodon Post","posts.create.steps.format.facebook_post":"Facebook Post","posts.create.steps.format.pinterest_pin":"Pinterest Pin","posts.create.steps.format.instagram_story":"Instagram Story","posts.create.steps.format.facebook_story":"Facebook Story","posts.templates.browser_title":"Choose a template","posts.templates.browser_description":"Start from a curated template and adapt it.","posts.templates.search_placeholder":"Search templates…","posts.templates.no_search_results":"No templates match your search","posts.templates.try_different_search":"Try a different keyword or clear the search.","posts.templates.slides_count":"{count} slide|{count} slides","posts.templates.all_platforms":"All platforms","posts.templates.platform_search_placeholder":"Search platform…","posts.templates.no_platform_match":"No platform matches.","posts.templates.use_this":"Use this template","posts.templates.no_templates":"No templates available.","posts.templates.applying":"Applying template…","posts.templates.category.product_launch":"Product launch","posts.templates.category.promotion":"Promotion","posts.templates.category.educational":"Educational","posts.templates.category.behind_the_scenes":"Behind the scenes","posts.templates.category.testimonial":"Testimonial","posts.templates.category.industry_tip":"Industry tip","posts.templates.category.event":"Event","posts.templates.category.engagement":"Engagement","settings.title":"Settings","settings.description":"Manage your profile and account settings","settings.hub.title":"Settings","settings.hub.description":"Choose what you want to manage.","settings.hub.profile.title":"Profile","settings.hub.profile.description":"Update your personal info, password, and notification preferences.","settings.hub.workspace.title":"Workspace","settings.hub.workspace.description":"Configure your workspace, brand, members, and API keys.","settings.hub.account.title":"Account","settings.hub.account.description":"Manage your account info, usage, and billing.","settings.nav.profile":"Profile","settings.nav.authentication":"Authentication","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.authentication.title":"Authentication","settings.authentication.page_title":"Authentication settings","settings.authentication.sessions.title":"Active sessions","settings.authentication.sessions.description":"If you notice anything suspicious, sign out of other devices.","settings.authentication.sessions.unknown_browser":"Unknown browser","settings.authentication.sessions.unknown_ip":"Unknown IP","settings.authentication.sessions.on":"on","settings.authentication.sessions.active_now":"Active now","settings.authentication.sessions.log_out_others":"Log out other devices","settings.authentication.sessions.modal_title":"Log out other devices","settings.authentication.sessions.modal_description_password":"Enter your current password to confirm you want to log out other browser sessions.","settings.authentication.sessions.modal_description_email":"Type your email address to confirm you want to log out other browser sessions.","settings.authentication.sessions.password_placeholder":"Current password","settings.authentication.sessions.email_placeholder":"Your account email","settings.authentication.sessions.cancel":"Cancel","settings.authentication.sessions.submit":"Log out other devices","settings.authentication.sessions.email_mismatch":"The email address does not match your account.","settings.authentication.sessions.flash_logged_out":"You have been logged out from other devices.","settings.authentication.password.update_title":"Update password","settings.authentication.password.set_title":"Set a password","settings.authentication.password.update_description":"Ensure your account is using a long, random password to stay secure.","settings.authentication.password.set_description":"Add a password so you can sign in without a connected provider.","settings.authentication.password.current_password":"Current password","settings.authentication.password.new_password":"New password","settings.authentication.password.confirm_password":"Confirm password","settings.authentication.password.save":"Save password","settings.authentication.password.set":"Set password","settings.authentication.providers.title":"Connected accounts","settings.authentication.providers.description":"Sign in faster with these connected providers.","settings.authentication.providers.connected":"Connected","settings.authentication.providers.not_connected":"Not connected","settings.authentication.providers.connect":"Connect","settings.authentication.providers.disconnect":"Disconnect","settings.authentication.providers.flash_disconnected":":provider disconnected successfully.","settings.authentication.providers.flash_connected":":provider connected successfully.","settings.authentication.providers.flash_already_linked":"That :provider account is already linked to another user.","settings.authentication.providers.flash_cannot_disconnect":"You cannot disconnect your only sign-in method. Set a password or connect another provider first.","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_password":"Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your password to confirm.","settings.delete_account.modal_description_email":"Once your account is deleted, all of its resources and data will also be permanently deleted. Please type your email address :email to confirm.","settings.delete_account.password":"Password","settings.delete_account.password_placeholder":"Password","settings.delete_account.email_placeholder":"Your account email","settings.delete_account.email_mismatch":"The email address does not match your account.","settings.delete_account.cancel":"Cancel","settings.delete_account.confirm":"Delete account","settings.workspace.tabs.workspace":"Workspace","settings.workspace.tabs.brand":"Brand","settings.workspace.tabs.users":"Members","settings.workspace.tabs.api_keys":"API Keys","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.name":"Workspace name","settings.brand.name_placeholder":"My brand","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.brand_color":"Brand color","settings.brand.background_color":"Background color","settings.brand.text_color":"Text color","settings.brand.font":"Font","settings.brand.image_style":"Image style","settings.brand.image_style_description":"Visual style applied when generating slide and cover images for AI posts.","settings.brand.image_style_cinematic":"Cinematic","settings.brand.image_style_illustration":"Illustration","settings.brand.image_style_isometric_3d":"Isometric","settings.brand.image_style_cartoon":"Cartoon","settings.brand.image_style_typographic":"Typographic","settings.brand.image_style_infographic":"Infographic","settings.brand.image_style_minimalist":"Minimalist","settings.brand.image_style_mockup":"Mockup","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.roles.viewer":"Viewer","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.tabs.account":"Account","settings.account.tabs.usage":"Usage","settings.account.tabs.billing":"Billing","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.signatures":"Signatures","sidebar.workspace.labels":"Labels","sidebar.workspace.assets":"Assets","sidebar.workspace.api_keys":"API Keys","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.support":"Support","sidebar.analytics":"Analytics","sidebar.settings":"Settings","sidebar.posts.calendar":"Calendar","sidebar.posts.all":"All","sidebar.posts.scheduled":"Scheduled","sidebar.posts.posted":"Posted","sidebar.posts.drafts":"Drafts","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","signatures.title":"Signatures","signatures.description":"Create reusable signatures to quickly append to your posts","signatures.search":"Search signatures...","signatures.new":"New signature","signatures.empty_title":"No signatures yet","signatures.empty_description":"Create signatures to quickly append hashtags, links, or any reusable text to your posts","signatures.no_search_results":"No signatures match your search","signatures.try_different_search":"Try a different keyword or clear the search.","signatures.table.name":"Name","signatures.table.content":"Content","signatures.table.created_at":"Created","signatures.actions.edit":"Edit signature","signatures.actions.delete":"Delete signature","signatures.create.title":"Create signature","signatures.create.description":"Give your signature a name and the content to append (hashtags, links, custom text — anything you reuse).","signatures.create.name":"Name","signatures.create.name_placeholder":"e.g. Marketing, Travel, Brand sign-off","signatures.create.content":"Content","signatures.create.content_placeholder":"#marketing #socialmedia\nLearn more: https://yourbrand.com","signatures.create.content_hint":"Hashtags, links, custom intros, signoffs — anything you append to posts.","signatures.create.submit":"Create signature","signatures.create.submitting":"Creating...","signatures.edit.title":"Edit signature","signatures.edit.description":"Update the name and content for this signature.","signatures.edit.name":"Name","signatures.edit.name_placeholder":"e.g. Marketing, Travel, Brand sign-off","signatures.edit.content":"Content","signatures.edit.content_placeholder":"#marketing #socialmedia\nLearn more: https://yourbrand.com","signatures.edit.content_hint":"Hashtags, links, custom intros, signoffs — anything you append to posts.","signatures.edit.submit":"Save changes","signatures.edit.submitting":"Saving...","signatures.delete.title":"Delete signature","signatures.delete.description":"Are you sure you want to delete this signature? This action cannot be undone.","signatures.delete.confirm":"Delete","signatures.delete.cancel":"Cancel","signatures.flash.created":"Signature created.","signatures.flash.updated":"Signature updated.","signatures.flash.deleted":"Signature deleted.","usage.title":"Usage","usage.section_account":"Account","usage.section_account_description":"Quotas and limits for your :plan plan.","usage.section_ai":"AI Credits","usage.section_ai_description":"Credits are debited as AI features are used. They reset on the first of every month.","usage.workspaces":"Workspaces","usage.social_accounts":"Social Accounts","usage.members":"Members","usage.credits":"Credits","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 a bit about you or your project. We'll use it 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.brand_color":"Brand color","workspaces.create.background_color":"Background color","workspaces.create.text_color":"Text color","workspaces.create.submit":"Create workspace","workspaces.create.success":"Workspace created. Connect a social account to start posting.","workspaces.limit_reached":"You have reached your plan limit for workspaces.","workspaces.flash.deleted":"Workspace deleted successfully."} \ No newline at end of file +{"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.connect_cta":"Connect","accounts.no_accounts":"No accounts connected yet","accounts.no_accounts_description":"Connect your social networks to start scheduling and publishing posts","accounts.no_search_results":"No accounts match your search","accounts.try_different_search":"Try a different keyword or clear the search.","accounts.search":"Search accounts...","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.table.account":"Account","accounts.table.platform":"Platform","accounts.table.status":"Status","accounts.table.last_used":"Last used","accounts.table.added":"Added","accounts.table.active":"Active","accounts.never_used":"Never used","accounts.status.connected":"Connected","accounts.status.disconnected":"Disconnected","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.","accounts.popup_callback.title_success":"Connected","accounts.popup_callback.title_error":"Error","accounts.popup_callback.closing":"This window will close automatically...","accounts.popup_callback.close_now":"You can close this window now.","accounts.popup_callback.connected":"Account connected!","accounts.popup_callback.reconnected":"Account reconnected!","accounts.popup_callback.error_connecting":"Error connecting account. Please try again.","accounts.popup_callback.error_connecting_page":"Error connecting page. Please try again.","accounts.popup_callback.error_connecting_channel":"Error connecting channel. Please try again.","accounts.popup_callback.session_expired":"Session expired. Please try again.","accounts.popup_callback.workspace_not_found":"Workspace not found.","accounts.popup_callback.invalid_state":"Invalid state. Please try again.","accounts.popup_callback.failed_to_authenticate":"Failed to authenticate.","accounts.popup_callback.failed_to_get_profile":"Failed to get profile.","accounts.popup_callback.page_not_found":"Page not found.","accounts.popup_callback.channel_not_found":"Channel not found.","accounts.popup_callback.no_facebook_pages":"No Facebook Pages found. You need to be an admin of at least one page.","accounts.popup_callback.no_facebook_instagram_pages":"No Facebook Pages with linked Instagram accounts found.","accounts.popup_callback.no_youtube_channels":"No YouTube channels found. Please create a channel first.","accounts.popup_callback.not_linkedin_admin":"You are not an administrator of any LinkedIn page.","analytics.no_accounts":"No connected accounts with analytics.","analytics.no_accounts_match":"No accounts match.","analytics.search_account":"Search account…","analytics.select_account":"Select an account to view analytics.","analytics.no_data":"No analytics data available.","analytics.metrics.avg_view_duration":"Avg. View Duration (s)","analytics.metrics.avg_view_percentage":"Avg. View Percentage","analytics.metrics.bookmarks":"Bookmarks","analytics.metrics.clicks":"Clicks","analytics.metrics.comments":"Comments","analytics.metrics.engagement":"Engagement","analytics.metrics.favourites":"Favourites","analytics.metrics.followers":"Followers","analytics.metrics.following":"Following","analytics.metrics.impressions":"Impressions","analytics.metrics.interactions":"Interactions","analytics.metrics.likes":"Likes","analytics.metrics.minutes_watched":"Minutes Watched","analytics.metrics.organic_followers":"Organic Followers","analytics.metrics.outbound_clicks":"Outbound Clicks","analytics.metrics.page_followers":"Page Followers","analytics.metrics.page_reach":"Page Reach","analytics.metrics.page_views":"Page Views","analytics.metrics.paid_followers":"Paid Followers","analytics.metrics.pin_click_rate":"Pin Click Rate","analytics.metrics.pin_clicks":"Pin Clicks","analytics.metrics.posts_engagement":"Posts Engagement","analytics.metrics.posts_reach":"Posts Reach","analytics.metrics.quotes":"Quotes","analytics.metrics.reach":"Reach","analytics.metrics.reblogs":"Reblogs","analytics.metrics.recent_comments":"Recent Comments","analytics.metrics.recent_likes":"Recent Likes","analytics.metrics.recent_shares":"Recent Shares","analytics.metrics.replies":"Replies","analytics.metrics.reposts":"Reposts","analytics.metrics.retweets":"Retweets","analytics.metrics.saves":"Saves","analytics.metrics.shares":"Shares","analytics.metrics.subscribers_gained":"Subscribers Gained","analytics.metrics.subscribers_lost":"Subscribers Lost","analytics.metrics.total_likes":"Total Likes","analytics.metrics.video_views":"Video Views","analytics.metrics.videos":"Videos","analytics.metrics.views":"Views","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.add_to_post":"Add to post","assets.search_placeholder":"Search media...","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","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.signatures.title":"Signatures","auth.slides.signatures.description":"Save reusable signatures (hashtags, links, signoffs) and append them to posts with one click.","auth.or_continue_with":"Or continue with","auth.google_login":"Log in with Google","auth.google_signup":"Sign up with Google","auth.github_login":"Log in with GitHub","auth.github_signup":"Sign up with GitHub","auth.github_email_unavailable":"Unable to retrieve your email from GitHub. Make your GitHub email public or grant the email scope, then try again.","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.upgrade_dialog.title":"Upgrade your plan","billing.upgrade_dialog.description":"Pick a plan that fits your needs.","billing.upgrade_dialog.current_plan":"Current plan","billing.upgrade_dialog.current_short":"Current","billing.upgrade_dialog.current_badge":"Current","billing.upgrade_dialog.subscribe":"Subscribe","billing.upgrade_dialog.switch":"Switch to this plan","billing.upgrade_dialog.switch_short":"Switch","billing.upgrade_dialog.switch_to_yearly":"Switch to yearly","billing.upgrade_dialog.switch_to_monthly":"Switch to monthly","billing.upgrade_dialog.unavailable":"Unavailable","billing.upgrade_dialog.reasons.workspace_limit":"You've reached the workspace limit on your current plan. Upgrade to create more workspaces.","billing.upgrade_dialog.reasons.social_account_limit":"You've reached the social account limit on your current plan. Upgrade to connect more accounts.","billing.upgrade_dialog.reasons.member_limit":"You've reached the team member limit on your current plan. Upgrade to invite more people.","billing.subscribe.page_title":"Choose your plan","billing.subscribe.eyebrow":"Pricing","billing.subscribe.title":"Choose the right plan for you","billing.subscribe.description":"Pick the plan that fits you. Billed monthly or annually.","billing.subscribe.monthly":"Monthly","billing.subscribe.yearly":"Yearly","billing.subscribe.per_month":"monthly","billing.subscribe.per_year":"yearly","billing.subscribe.billed_monthly":"Billed monthly","billing.subscribe.billed_yearly":"Billed annually","billing.subscribe.features_included":"What's included:","billing.subscribe.everything_in":"Everything in :plan, plus:","billing.subscribe.save_months":"2 months free","billing.subscribe.popular":"Most popular","billing.subscribe.subscribe_cta":"Subscribe","billing.subscribe.prices.starter.monthly":"$19","billing.subscribe.prices.starter.yearly_per_month":"$16","billing.subscribe.prices.starter.yearly":"$190","billing.subscribe.prices.plus.monthly":"$29","billing.subscribe.prices.plus.yearly_per_month":"$24","billing.subscribe.prices.plus.yearly":"$290","billing.subscribe.prices.pro.monthly":"$49","billing.subscribe.prices.pro.yearly_per_month":"$41","billing.subscribe.prices.pro.yearly":"$490","billing.subscribe.prices.max.monthly":"$99","billing.subscribe.prices.max.yearly_per_month":"$83","billing.subscribe.prices.max.yearly":"$990","billing.subscribe.features.social_accounts":":count social accounts","billing.subscribe.features.workspaces":":count workspaces","billing.subscribe.features.members":":count team members","billing.subscribe.features.credits":":count AI credits/mo","billing.subscribe.credit_tooltips.starter":"Roughly 150 medium-length posts plus 5 AI images per month.","billing.subscribe.credit_tooltips.plus":"Roughly 300 medium-length posts plus 10 AI images per month.","billing.subscribe.credit_tooltips.pro":"Roughly 700 medium-length posts plus 30 AI images per month.","billing.subscribe.credit_tooltips.max":"Roughly 2,000 medium-length posts plus 100 AI images per month.","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.no_payment_method":"No payment method on file yet.","billing.subscription.expires_on":"Expires :month/:year","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.flash.plan_changed":"You are now on the :plan plan.","billing.flash.cannot_manage":"Only the account owner can manage billing.","billing.flash.cannot_downgrade.workspaces":"Cannot switch to :plan: you have :count workspaces but the plan only allows :limit.","billing.flash.cannot_downgrade.social_accounts":"Cannot switch to :plan: you have :count social accounts but the plan only allows :limit.","billing.flash.cannot_downgrade.members":"Cannot switch to :plan: you have :count team members (including invites) but the plan only allows :limit.","billing.flash.credits_exhausted":"Out of AI credits — your monthly :limit allowance has been used. Upgrade your plan or wait until next month.","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","comments.today":"Today","comments.yesterday":"Yesterday","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.confirm_modal.delete_keyword":"delete","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.date_range_picker.placeholder":"Pick a date range","common.date_range_picker.today":"Today","common.date_range_picker.yesterday":"Yesterday","common.date_range_picker.last_7_days":"Last 7 days","common.date_range_picker.last_30_days":"Last 30 days","common.date_range_picker.last_3_months":"Last 3 months","common.date_range_picker.last_6_months":"Last 6 months","common.date_range_picker.last_12_months":"Last 12 months","common.date_range_picker.this_month":"This month","common.date_range_picker.last_month":"Last month","common.date_range_picker.year_to_date":"Year to date","common.date_range_picker.last_year":"Last year","common.cancel":"Cancel","common.clear":"Clear","common.close":"Close","common.loading_more":"Loading more...","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.no_search_results":"No labels match your search","labels.try_different_search":"Try a different keyword or clear the search.","labels.create_first_label":"Create your first label","labels.table.name":"Name","labels.table.created_at":"Created","labels.actions.edit":"Edit label","labels.actions.delete":"Delete 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.mentioned.subject":":name mentioned you on TryPost","mail.mentioned.title":":name mentioned you","mail.mentioned.intro":":name mentioned you in a post comment.","mail.mentioned.cta":"View comment","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","notifications.post_ready.title":"Your post is ready","notifications.post_ready.body":"The AI just finished. Tap to review and publish.","notifications.account_disconnected.title":":platform account disconnected","notifications.account_disconnected.body":":account needs to be reconnected","notifications.account_token_expired.title":":platform account needs to be reconnected","notifications.account_token_expired.body":":account session expired — please reconnect to keep posting","posts.title":"Posts","posts.search":"Search posts...","posts.all_posts":"All Posts","posts.new_post":"New Post","posts.no_posts":"No posts found","posts.no_search_results":"No posts match your search","posts.try_different_search":"Try a different keyword or clear the search.","posts.start_creating":"Start by creating your first post.","posts.filter_by_label":"Filter by label","posts.label_search_placeholder":"Search labels...","posts.no_labels":"No labels found.","posts.clear_label_filter":"Clear label filter","posts.table.post":"Post","posts.table.status":"Status","posts.table.content":"Content","posts.table.platforms":"Platforms","posts.table.labels":"Labels","posts.table.scheduled_at":"Date","posts.table.actions":"","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","posts.actions.duplicate":"Duplicate","posts.actions.copy_id":"Copy ID","posts.actions.copied":"ID copied to clipboard","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.content_exceeds_platform":":platform: too long by :over chars (max :limit).","posts.form.tiktok.settings":"TikTok Settings","posts.form.tiktok.variant_label":"Post type","posts.form.tiktok.variant.video":"Video","posts.form.tiktok.variant.photo":"Photo carousel","posts.form.tiktok.posting_to":"Posting to","posts.form.tiktok.privacy_level":"Who can see this video?","posts.form.tiktok.privacy_placeholder":"Select visibility","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.private_disabled_branded":"Branded content visibility cannot be set to private.","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.disclose":"Disclose video content","posts.form.tiktok.disclose_hint":"Turn on to disclose that this video promotes goods or services in exchange for something of value. Your video could promote yourself, a third party, or both.","posts.form.tiktok.promotional_organic_title":"Your photo/video will be labeled as \"Promotional content\".","posts.form.tiktok.promotional_paid_title":"Your photo/video will be labeled as \"Paid partnership\".","posts.form.tiktok.promotional_description":"This cannot be changed once your video is posted.","posts.form.tiktok.compliance_incomplete":"You need to indicate if your content promotes yourself, a third party, or both.","posts.form.tiktok.privacy_required":"TikTok privacy level is required when publishing.","posts.form.tiktok.branded_cleared_private":"Privacy was cleared because Branded Content cannot be private.","posts.form.tiktok.interaction_disabled_by_creator":"Disabled by your TikTok account settings.","posts.form.tiktok.max_duration_exceeded":"Video is :duration s long but this account can only post videos up to :max s.","posts.form.tiktok.processing_hint":"After publishing, it may take a few minutes for the content to process and appear on your TikTok profile.","posts.form.tiktok.brand_organic":"Your brand","posts.form.tiktok.brand_organic_hint":"You are promoting yourself or your own brand. This video will be classified as Brand Organic.","posts.form.tiktok.brand_content":"Branded content","posts.form.tiktok.brand_content_hint":"You are promoting another brand or a third party. This video will be classified as Branded Content.","posts.form.tiktok.compliance.agree":"By posting, you agree to TikTok's","posts.form.tiktok.compliance.music_usage":"Music Usage Confirmation","posts.form.tiktok.compliance.and":"and","posts.form.tiktok.compliance.branded_policy":"Branded Content Policy","posts.form.instagram.settings":"Instagram Settings","posts.form.instagram.posting_to":"Posting to","posts.form.instagram.variant_label":"Post type","posts.form.instagram.variant.feed":"Feed Post","posts.form.instagram.variant.reel":"Reel","posts.form.instagram.variant.story":"Story","posts.form.instagram.aspect_label":"Aspect ratio","posts.form.instagram.aspect.square":"Square (1:1)","posts.form.instagram.aspect.portrait":"Portrait (4:5)","posts.form.instagram.aspect.landscape":"Landscape (16:9)","posts.form.instagram.aspect.original":"Original","posts.form.facebook.settings":"Facebook Settings","posts.form.facebook.posting_to":"Posting to","posts.form.facebook.variant_label":"Post type","posts.form.facebook.variant.post":"Post","posts.form.facebook.variant.reel":"Reel","posts.form.facebook.variant.story":"Story","posts.form.linkedin.settings":"LinkedIn Settings","posts.form.linkedin.settings_page":"LinkedIn Page Settings","posts.form.linkedin.posting_to":"Posting to","posts.form.linkedin.variant_label":"Post type","posts.form.linkedin.variant.post":"Post","posts.form.linkedin.variant.carousel":"Carousel","posts.form.pinterest.settings":"Pinterest Settings","posts.form.pinterest.posting_to":"Posting to","posts.form.pinterest.variant_label":"Pin type","posts.form.pinterest.variant.pin":"Pin","posts.form.pinterest.variant.video_pin":"Video Pin","posts.form.pinterest.variant.carousel":"Carousel","posts.form.pinterest.board":"Board","posts.form.pinterest.select_board":"Select a board","posts.form.pinterest.no_boards":"No Pinterest boards found. Create one in your Pinterest account first.","posts.form.pinterest.search_board":"Search boards...","posts.form.pinterest.no_board_found":"No board matches your search.","posts.form.pinterest.board_required":"Select a Pinterest board to publish this post.","posts.form.warnings.no_variant":"Pick a post type to continue.","posts.form.warnings.requires_media":"This post type requires at least one image or video.","posts.form.warnings.max_files_exceeded":"This post type accepts up to :max media files (you have :current).","posts.form.warnings.min_files_required":"This post type requires at least :min media files (you have :current).","posts.form.warnings.no_video_allowed":"This post type does not accept videos.","posts.form.warnings.no_image_allowed":"This post type accepts only videos.","posts.form.warnings.gif_not_allowed":"This platform does not accept GIF. Remove the GIF or choose a different network.","posts.form.warnings.image_too_large":"Image exceeds the :max limit for this post type (yours is :current).","posts.form.warnings.video_too_large":"Video exceeds the :max limit for this post type (yours is :current).","posts.form.warnings.video_too_long":"Video is :current long, but this post type allows up to :max.","posts.form.warnings.aspect_ratio_too_narrow":"Aspect ratio :current is too tall for this post type (min :min).","posts.form.warnings.aspect_ratio_too_wide":"Aspect ratio :current is too wide for this post type (max :max).","posts.status.pending":"Pending","posts.status.draft":"Draft","posts.status.scheduled":"Scheduled","posts.status.publishing":"Publishing","posts.status.retrying":"Retrying","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.ai.generate.button_tooltip":"Generate with AI","posts.ai.generate.title":"Generate post with AI","posts.ai.generate.description":"Describe what the post should be about. The AI will use your brand context to write it.","posts.ai.generate.prompt_label":"What is this post about?","posts.ai.generate.prompt_placeholder":"e.g. Announce our new image-generation feature for carousels","posts.ai.generate.preview_label":"Preview","posts.ai.generate.start":"Generate","posts.ai.generate.apply":"Use this content","posts.ai.generate.retry":"Try again","posts.ai.generate.cancel":"Cancel","posts.ai.review.button_tooltip":"Review with AI","posts.ai.review.title":"Review post with AI","posts.ai.review.description":"AI scans for grammar, spelling, and clarity. Apply the suggestions you agree with.","posts.ai.review.loading":"Reviewing your text...","posts.ai.review.no_issues":"No issues found. Looks good.","posts.ai.review.original":"Original","posts.ai.review.suggestion":"Suggestion","posts.ai.review.apply":"Apply","posts.ai.review.apply_all":"Apply all","posts.ai.review.applied":"Applied","posts.ai.review.cancel":"Cancel","posts.show.title":"Post Details","posts.show.edit":"Edit","posts.show.back":"Back","posts.show.no_content":"No caption","posts.show.platforms":"Platforms","posts.show.no_platforms":"No platforms selected.","posts.show.view_on_platform":"View on platform","posts.show.published_on":"Published on :date","posts.show.scheduled_for":"Scheduled for :date","posts.show.draft":"Draft","posts.show.status_pending":"Pending","posts.show.metrics":"Metrics","posts.show.metrics_loading":"Loading metrics…","posts.show.metrics_unavailable":"Metrics unavailable for this platform yet.","posts.show.metrics_empty":"No metrics returned.","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.schedule_date":"Schedule date","posts.edit.unschedule":"Unschedule","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.preview_empty.title":"No platform selected","posts.edit.preview_empty.description":"Select a platform to publish to see the preview.","posts.edit.drop_zone_title":"Add media","posts.edit.drop_zone_subtitle":"Drag & drop files or click to browse","posts.edit.add":"Add","posts.edit.publish_to":"Publish to","posts.edit.organize":"Organize","posts.edit.signatures":"Signatures","posts.edit.view_on_platform":"View on platform","posts.edit.platform_status":"Platform status","posts.edit.compliance_incomplete":"Some platform settings are incomplete or incompatible with the attached media.","posts.edit.compliance.requires_media":"Add an image or video to publish here.","posts.edit.compliance.too_many_files":"Only :max file(s) allowed for this format.","posts.edit.compliance.too_few_files":"Add at least :min files for this format.","posts.edit.compliance.no_videos":"Only images are allowed for this format.","posts.edit.compliance.no_images":"Only videos are allowed for this format.","posts.edit.compliance.no_gifs":"GIFs are not supported here.","posts.edit.compliance.video_too_large":"Video exceeds the size limit for this platform.","posts.edit.compliance.video_too_long":"Video must be under :seconds seconds for this format.","posts.edit.compliance.image_too_large":"Image exceeds the size limit for this platform.","posts.edit.compliance.aspect_ratio_invalid":"Aspect ratio is not supported by this format.","posts.edit.compliance.no_content_type":"Pick a content type for this platform.","posts.edit.publishing":"Publishing...","posts.edit.publishing_overlay_title":"Your post is being published","posts.edit.publishing_overlay_subtitle":"This can take a few moments. You can safely leave this page.","posts.edit.scheduled_overlay_title":"This post is scheduled","posts.edit.scheduled_overlay_subtitle":"Scheduled for :date. Unschedule it first to make changes.","posts.edit.unschedule_cta":"Unschedule to edit","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.media_picker.title":"Pick from gallery","posts.edit.media_picker.search":"Search media...","posts.edit.media_picker.empty":"No media in your gallery yet","posts.edit.media_picker.cancel":"Cancel","posts.edit.media_picker.add":"Add","posts.edit.media_picker.add_count":"Add :count","posts.edit.emoji_picker.search":"Search emoji","posts.edit.emoji_picker.empty":"No emojis found","posts.edit.emoji_picker.recent":"Frequently used","posts.edit.emoji_picker.smileys":"Smileys & emotion","posts.edit.emoji_picker.people":"People & body","posts.edit.emoji_picker.nature":"Animals & nature","posts.edit.emoji_picker.food":"Food & drink","posts.edit.emoji_picker.activities":"Activities","posts.edit.emoji_picker.travel":"Travel & places","posts.edit.emoji_picker.objects":"Objects","posts.edit.emoji_picker.symbols":"Symbols","posts.edit.emoji_picker.flags":"Flags","posts.edit.status.scheduled":"Scheduled","posts.edit.status.published":"Published","posts.edit.status.publishing":"Publishing...","posts.edit.status.retrying":"Retrying...","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.signatures_modal.search":"Search signatures...","posts.edit.signatures_modal.no_results":"No signatures 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.tiktok_photo.label":"Photo carousel","posts.content_types.tiktok_photo.description":"Up to 35 photos as a swipeable carousel","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":"Standard image pin","posts.content_types.pinterest_video_pin.label":"Video Pin","posts.content_types.pinterest_video_pin.description":"Video pin (4s - 15min)","posts.content_types.pinterest_carousel.label":"Carousel","posts.content_types.pinterest_carousel.description":"Multi-image carousel (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.deleted":"Post deleted successfully!","posts.flash.duplicated":"Post duplicated as a draft.","posts.flash.cannot_edit_published":"Published posts cannot be edited.","posts.flash.cannot_delete_published":"Published posts cannot be deleted.","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","posts.errors.account_token_expired":"Social account session expired — please reconnect","posts.delete.title":"Delete post?","posts.delete.description":"This action can't be undone. The post and all its media will be permanently removed.","posts.delete.confirm":"Yes, delete","posts.delete.cancel":"Cancel","posts.create.title":"Create a new post","posts.create.description":"Choose how you want to start.","posts.create.scratch_title":"Start from scratch","posts.create.scratch_description":"Open a blank post and write everything yourself.","posts.create.ai_title":"Generate with AI","posts.create.ai_description":"Describe what you want and AI generates the content for you.","posts.create.ai_configure_description":"Pick a format and describe the post you want to create.","posts.create.template_title":"Use a template","posts.create.template_description":"Pick from our curated templates and customize.","posts.create.coming_soon":"Coming soon","posts.create.preview.image_title":"Image title","posts.create.preview.image_body":"Image body","posts.create.steps.format_title":"Choose a format","posts.create.steps.format_description":"Select the type of post you want to create.","posts.create.steps.account_title":"Choose an account","posts.create.steps.account_description":"Select the social account to publish to.","posts.create.steps.media_title":"Media options","posts.create.steps.media_carousel":"How many slides?","posts.create.steps.media_optional":"Include images?","posts.create.steps.media_optional_label":"How many images?","posts.create.steps.media_none":"None","posts.create.steps.media_count_label":"Number of images","posts.create.steps.prompt_title":"Describe your post","posts.create.steps.prompt_label":"What is this post about?","posts.create.steps.prompt_placeholder":"e.g. Announce our new carousel feature for Instagram","posts.create.steps.preview_error":"Something went wrong. Please try again.","posts.create.steps.loading_page_title":"Generating your post","posts.create.steps.loading_eta":"Estimated time: about :minutes.","posts.create.steps.loading_eta_minute_one":"1 minute","posts.create.steps.loading_eta_minute_other":":count minutes","posts.create.steps.loading_leave_title":"You can keep working.","posts.create.steps.loading_leave_body":"We will notify you when the post is ready.","posts.create.steps.loading_leave_cta":"Go to calendar","posts.create.steps.loading_create_another_cta":"Create another post","posts.create.steps.loading_tip_credits":"Each AI image uses about 15 credits.","posts.create.steps.loading_tip_edit":"You will be able to edit everything once the post is ready.","posts.create.steps.loading_tip_draft":"Generated posts land in your drafts.","posts.create.steps.loading_tip_brand":"Tweak your brand settings to influence future posts.","posts.create.steps.loading_tip_carousel":"Carousels deliver one slide per uploaded image.","posts.create.steps.loading_tip_quality":"Image quality is set to balance speed and cost.","posts.create.steps.create":"Create post","posts.create.steps.back":"Back","posts.create.steps.next":"Continue","posts.create.steps.cancel":"Cancel","posts.create.steps.discard":"Discard","posts.create.steps.retry":"Try again","posts.create.steps.no_platforms":"No connected accounts","posts.create.steps.connect_first":"Connect at least one social account to use AI generation.","posts.create.steps.format.instagram_feed":"Instagram Feed Post","posts.create.steps.format.instagram_carousel":"Instagram Carousel","posts.create.steps.format.linkedin_post":"LinkedIn Post","posts.create.steps.format.linkedin_page_post":"LinkedIn Page Post","posts.create.steps.format.x_post":"X Post","posts.create.steps.format.bluesky_post":"Bluesky Post","posts.create.steps.format.threads_post":"Threads Post","posts.create.steps.format.mastodon_post":"Mastodon Post","posts.create.steps.format.facebook_post":"Facebook Post","posts.create.steps.format.pinterest_pin":"Pinterest Pin","posts.create.steps.format.instagram_story":"Instagram Story","posts.create.steps.format.facebook_story":"Facebook Story","posts.templates.browser_title":"Choose a template","posts.templates.browser_description":"Start from a curated template and adapt it.","posts.templates.search_placeholder":"Search templates…","posts.templates.no_search_results":"No templates match your search","posts.templates.try_different_search":"Try a different keyword or clear the search.","posts.templates.slides_count":"{count} slide|{count} slides","posts.templates.all_platforms":"All platforms","posts.templates.platform_search_placeholder":"Search platform…","posts.templates.no_platform_match":"No platform matches.","posts.templates.use_this":"Use this template","posts.templates.no_templates":"No templates available.","posts.templates.applying":"Applying template…","posts.templates.category.product_launch":"Product launch","posts.templates.category.promotion":"Promotion","posts.templates.category.educational":"Educational","posts.templates.category.behind_the_scenes":"Behind the scenes","posts.templates.category.testimonial":"Testimonial","posts.templates.category.industry_tip":"Industry tip","posts.templates.category.event":"Event","posts.templates.category.engagement":"Engagement","settings.title":"Settings","settings.description":"Manage your profile and account settings","settings.hub.title":"Settings","settings.hub.description":"Choose what you want to manage.","settings.hub.profile.title":"Profile","settings.hub.profile.description":"Update your personal info, password, and notification preferences.","settings.hub.workspace.title":"Workspace","settings.hub.workspace.description":"Configure your workspace, brand, members, and API keys.","settings.hub.account.title":"Account","settings.hub.account.description":"Manage your account info, usage, and billing.","settings.nav.profile":"Profile","settings.nav.authentication":"Authentication","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.authentication.title":"Authentication","settings.authentication.page_title":"Authentication settings","settings.authentication.sessions.title":"Active sessions","settings.authentication.sessions.description":"If you notice anything suspicious, sign out of other devices.","settings.authentication.sessions.unknown_browser":"Unknown browser","settings.authentication.sessions.unknown_ip":"Unknown IP","settings.authentication.sessions.on":"on","settings.authentication.sessions.active_now":"Active now","settings.authentication.sessions.log_out_others":"Log out other devices","settings.authentication.sessions.modal_title":"Log out other devices","settings.authentication.sessions.modal_description_password":"Enter your current password to confirm you want to log out other browser sessions.","settings.authentication.sessions.modal_description_email":"Type your email address to confirm you want to log out other browser sessions.","settings.authentication.sessions.password_placeholder":"Current password","settings.authentication.sessions.email_placeholder":"Your account email","settings.authentication.sessions.cancel":"Cancel","settings.authentication.sessions.submit":"Log out other devices","settings.authentication.sessions.email_mismatch":"The email address does not match your account.","settings.authentication.sessions.flash_logged_out":"You have been logged out from other devices.","settings.authentication.password.update_title":"Update password","settings.authentication.password.set_title":"Set a password","settings.authentication.password.update_description":"Ensure your account is using a long, random password to stay secure.","settings.authentication.password.set_description":"Add a password so you can sign in without a connected provider.","settings.authentication.password.current_password":"Current password","settings.authentication.password.new_password":"New password","settings.authentication.password.confirm_password":"Confirm password","settings.authentication.password.save":"Save password","settings.authentication.password.set":"Set password","settings.authentication.providers.title":"Connected accounts","settings.authentication.providers.description":"Sign in faster with these connected providers.","settings.authentication.providers.connected":"Connected","settings.authentication.providers.not_connected":"Not connected","settings.authentication.providers.connect":"Connect","settings.authentication.providers.disconnect":"Disconnect","settings.authentication.providers.flash_disconnected":":provider disconnected successfully.","settings.authentication.providers.flash_connected":":provider connected successfully.","settings.authentication.providers.flash_already_linked":"That :provider account is already linked to another user.","settings.authentication.providers.flash_cannot_disconnect":"You cannot disconnect your only sign-in method. Set a password or connect another provider first.","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_password":"Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your password to confirm.","settings.delete_account.modal_description_email":"Once your account is deleted, all of its resources and data will also be permanently deleted. Please type your email address :email to confirm.","settings.delete_account.password":"Password","settings.delete_account.password_placeholder":"Password","settings.delete_account.email_placeholder":"Your account email","settings.delete_account.email_mismatch":"The email address does not match your account.","settings.delete_account.cancel":"Cancel","settings.delete_account.confirm":"Delete account","settings.workspace.tabs.workspace":"Workspace","settings.workspace.tabs.brand":"Brand","settings.workspace.tabs.users":"Members","settings.workspace.tabs.api_keys":"API Keys","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.name":"Workspace name","settings.brand.name_placeholder":"My brand","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.brand_color":"Brand color","settings.brand.background_color":"Background color","settings.brand.text_color":"Text color","settings.brand.font":"Font","settings.brand.image_style":"Image style","settings.brand.image_style_description":"Visual style applied when generating slide and cover images for AI posts.","settings.brand.image_style_cinematic":"Cinematic","settings.brand.image_style_illustration":"Illustration","settings.brand.image_style_isometric_3d":"Isometric","settings.brand.image_style_cartoon":"Cartoon","settings.brand.image_style_typographic":"Typographic","settings.brand.image_style_infographic":"Infographic","settings.brand.image_style_minimalist":"Minimalist","settings.brand.image_style_mockup":"Mockup","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.roles.viewer":"Viewer","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.tabs.account":"Account","settings.account.tabs.usage":"Usage","settings.account.tabs.billing":"Billing","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.signatures":"Signatures","sidebar.workspace.labels":"Labels","sidebar.workspace.assets":"Assets","sidebar.workspace.api_keys":"API Keys","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.support":"Support","sidebar.analytics":"Analytics","sidebar.settings":"Settings","sidebar.posts.calendar":"Calendar","sidebar.posts.all":"All","sidebar.posts.scheduled":"Scheduled","sidebar.posts.posted":"Posted","sidebar.posts.drafts":"Drafts","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","signatures.title":"Signatures","signatures.description":"Create reusable signatures to quickly append to your posts","signatures.search":"Search signatures...","signatures.new":"New signature","signatures.empty_title":"No signatures yet","signatures.empty_description":"Create signatures to quickly append hashtags, links, or any reusable text to your posts","signatures.no_search_results":"No signatures match your search","signatures.try_different_search":"Try a different keyword or clear the search.","signatures.table.name":"Name","signatures.table.content":"Content","signatures.table.created_at":"Created","signatures.actions.edit":"Edit signature","signatures.actions.delete":"Delete signature","signatures.create.title":"Create signature","signatures.create.description":"Give your signature a name and the content to append (hashtags, links, custom text — anything you reuse).","signatures.create.name":"Name","signatures.create.name_placeholder":"e.g. Marketing, Travel, Brand sign-off","signatures.create.content":"Content","signatures.create.content_placeholder":"#marketing #socialmedia\nLearn more: https://yourbrand.com","signatures.create.content_hint":"Hashtags, links, custom intros, signoffs — anything you append to posts.","signatures.create.submit":"Create signature","signatures.create.submitting":"Creating...","signatures.edit.title":"Edit signature","signatures.edit.description":"Update the name and content for this signature.","signatures.edit.name":"Name","signatures.edit.name_placeholder":"e.g. Marketing, Travel, Brand sign-off","signatures.edit.content":"Content","signatures.edit.content_placeholder":"#marketing #socialmedia\nLearn more: https://yourbrand.com","signatures.edit.content_hint":"Hashtags, links, custom intros, signoffs — anything you append to posts.","signatures.edit.submit":"Save changes","signatures.edit.submitting":"Saving...","signatures.delete.title":"Delete signature","signatures.delete.description":"Are you sure you want to delete this signature? This action cannot be undone.","signatures.delete.confirm":"Delete","signatures.delete.cancel":"Cancel","signatures.flash.created":"Signature created.","signatures.flash.updated":"Signature updated.","signatures.flash.deleted":"Signature deleted.","usage.title":"Usage","usage.section_account":"Account","usage.section_account_description":"Quotas and limits for your :plan plan.","usage.section_ai":"AI Credits","usage.section_ai_description":"Credits are debited as AI features are used. They reset on the first of every month.","usage.workspaces":"Workspaces","usage.social_accounts":"Social Accounts","usage.members":"Members","usage.credits":"Credits","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 a bit about you or your project. We'll use it 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.brand_color":"Brand color","workspaces.create.background_color":"Background color","workspaces.create.text_color":"Text color","workspaces.create.submit":"Create workspace","workspaces.create.success":"Workspace created. Connect a social account to start posting.","workspaces.limit_reached":"You have reached your plan limit for workspaces.","workspaces.flash.deleted":"Workspace deleted successfully."} \ No newline at end of file diff --git a/lang/php_es.json b/lang/php_es.json index c2a4ed76..32d3a0dd 100644 --- a/lang/php_es.json +++ b/lang/php_es.json @@ -1 +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.connect_cta":"Conectar","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.no_search_results":"Ninguna cuenta coincide con tu búsqueda","accounts.try_different_search":"Prueba otra palabra clave o limpia la búsqueda.","accounts.search":"Buscar cuentas...","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.table.account":"Cuenta","accounts.table.platform":"Plataforma","accounts.table.status":"Estado","accounts.table.last_used":"Último uso","accounts.table.added":"Añadida","accounts.table.active":"Activa","accounts.never_used":"Nunca usada","accounts.status.connected":"Conectada","accounts.status.disconnected":"Desconectada","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.","accounts.popup_callback.title_success":"Conectado","accounts.popup_callback.title_error":"Error","accounts.popup_callback.closing":"Esta ventana se cerrará automáticamente...","accounts.popup_callback.close_now":"Puedes cerrar esta ventana ahora.","accounts.popup_callback.connected":"¡Cuenta conectada!","accounts.popup_callback.reconnected":"¡Cuenta reconectada!","accounts.popup_callback.error_connecting":"Error al conectar la cuenta. Inténtalo de nuevo.","accounts.popup_callback.error_connecting_page":"Error al conectar la página. Inténtalo de nuevo.","accounts.popup_callback.error_connecting_channel":"Error al conectar el canal. Inténtalo de nuevo.","accounts.popup_callback.session_expired":"Sesión expirada. Inténtalo de nuevo.","accounts.popup_callback.workspace_not_found":"Workspace no encontrado.","accounts.popup_callback.invalid_state":"Estado inválido. Inténtalo de nuevo.","accounts.popup_callback.failed_to_authenticate":"Falló la autenticación.","accounts.popup_callback.failed_to_get_profile":"Falló al obtener el perfil.","accounts.popup_callback.page_not_found":"Página no encontrada.","accounts.popup_callback.channel_not_found":"Canal no encontrado.","accounts.popup_callback.no_facebook_pages":"No se encontraron páginas de Facebook. Debes ser administrador de al menos una página.","accounts.popup_callback.no_facebook_instagram_pages":"No se encontraron páginas de Facebook con cuentas de Instagram vinculadas.","accounts.popup_callback.no_youtube_channels":"No se encontraron canales de YouTube. Crea un canal primero.","accounts.popup_callback.not_linkedin_admin":"No eres administrador de ninguna página de LinkedIn.","analytics.no_accounts":"No hay cuentas conectadas con analytics.","analytics.no_accounts_match":"Ninguna cuenta coincide.","analytics.search_account":"Buscar cuenta…","analytics.select_account":"Selecciona una cuenta para ver analytics.","analytics.no_data":"No hay datos de analytics disponibles.","analytics.metrics.avg_view_duration":"Duración Media (s)","analytics.metrics.avg_view_percentage":"Porcentaje Medio de Visualización","analytics.metrics.bookmarks":"Guardados","analytics.metrics.clicks":"Clics","analytics.metrics.comments":"Comentarios","analytics.metrics.engagement":"Engagement","analytics.metrics.favourites":"Favoritos","analytics.metrics.followers":"Seguidores","analytics.metrics.following":"Siguiendo","analytics.metrics.impressions":"Impresiones","analytics.metrics.interactions":"Interacciones","analytics.metrics.likes":"Me gusta","analytics.metrics.minutes_watched":"Minutos Vistos","analytics.metrics.organic_followers":"Seguidores Orgánicos","analytics.metrics.outbound_clicks":"Clics Externos","analytics.metrics.page_followers":"Seguidores de la Página","analytics.metrics.page_reach":"Alcance de la Página","analytics.metrics.page_views":"Vistas de la Página","analytics.metrics.paid_followers":"Seguidores Pagados","analytics.metrics.pin_click_rate":"Tasa de Clics en Pines","analytics.metrics.pin_clicks":"Clics en Pines","analytics.metrics.posts_engagement":"Engagement de Publicaciones","analytics.metrics.posts_reach":"Alcance de Publicaciones","analytics.metrics.quotes":"Citas","analytics.metrics.reach":"Alcance","analytics.metrics.reblogs":"Reblogs","analytics.metrics.recent_comments":"Comentarios Recientes","analytics.metrics.recent_likes":"Me Gusta Recientes","analytics.metrics.recent_shares":"Compartidos Recientes","analytics.metrics.replies":"Respuestas","analytics.metrics.reposts":"Reposts","analytics.metrics.retweets":"Retweets","analytics.metrics.saves":"Guardados","analytics.metrics.shares":"Compartidos","analytics.metrics.subscribers_gained":"Suscriptores Ganados","analytics.metrics.subscribers_lost":"Suscriptores Perdidos","analytics.metrics.total_likes":"Total de Me gusta","analytics.metrics.video_views":"Vistas de Vídeo","analytics.metrics.videos":"Vídeos","analytics.metrics.views":"Vistas","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.add_to_post":"Agregar al post","assets.search_placeholder":"Buscar media...","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","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.signatures.title":"Firmas","auth.slides.signatures.description":"Guarda firmas reutilizables (hashtags, links, despedidas) y añádelas a tus posts con un clic.","auth.or_continue_with":"O continuar con","auth.google_login":"Iniciar sesión con Google","auth.google_signup":"Registrarse con Google","auth.github_login":"Iniciar sesión con GitHub","auth.github_signup":"Registrarse con GitHub","auth.github_email_unavailable":"No fue posible obtener tu correo de GitHub. Haz tu correo público en GitHub o concede el permiso de correo y vuelve a intentar.","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":"Facturación","billing.upgrade_dialog.title":"Actualiza tu plan","billing.upgrade_dialog.description":"Elige un plan que se adapte a tus necesidades.","billing.upgrade_dialog.current_plan":"Plan actual","billing.upgrade_dialog.current_short":"Actual","billing.upgrade_dialog.current_badge":"Actual","billing.upgrade_dialog.subscribe":"Suscribirse","billing.upgrade_dialog.switch":"Cambiar a este plan","billing.upgrade_dialog.switch_short":"Cambiar","billing.upgrade_dialog.switch_to_yearly":"Cambiar a anual","billing.upgrade_dialog.switch_to_monthly":"Cambiar a mensual","billing.upgrade_dialog.unavailable":"No disponible","billing.upgrade_dialog.reasons.workspace_limit":"Has alcanzado el límite de workspaces de tu plan. Actualiza para crear más.","billing.upgrade_dialog.reasons.social_account_limit":"Has alcanzado el límite de cuentas sociales de tu plan. Actualiza para conectar más.","billing.upgrade_dialog.reasons.member_limit":"Has alcanzado el límite de miembros de tu plan. Actualiza para invitar a más personas.","billing.subscribe.page_title":"Elige tu plan","billing.subscribe.eyebrow":"Precios","billing.subscribe.title":"Elige el plan ideal para ti","billing.subscribe.description":"Elige el plan que te queda. Facturación mensual o anual.","billing.subscribe.monthly":"Mensual","billing.subscribe.yearly":"Anual","billing.subscribe.per_month":"mensual","billing.subscribe.per_year":"anual","billing.subscribe.billed_monthly":"Facturado mensualmente","billing.subscribe.billed_yearly":"Facturado anualmente","billing.subscribe.features_included":"Qué incluye:","billing.subscribe.everything_in":"Todo lo de :plan, más:","billing.subscribe.save_months":"2 meses gratis","billing.subscribe.popular":"Más popular","billing.subscribe.subscribe_cta":"Suscribirse","billing.subscribe.prices.starter.monthly":"$19","billing.subscribe.prices.starter.yearly_per_month":"$16","billing.subscribe.prices.starter.yearly":"$190","billing.subscribe.prices.plus.monthly":"$29","billing.subscribe.prices.plus.yearly_per_month":"$24","billing.subscribe.prices.plus.yearly":"$290","billing.subscribe.prices.pro.monthly":"$49","billing.subscribe.prices.pro.yearly_per_month":"$41","billing.subscribe.prices.pro.yearly":"$490","billing.subscribe.prices.max.monthly":"$99","billing.subscribe.prices.max.yearly_per_month":"$83","billing.subscribe.prices.max.yearly":"$990","billing.subscribe.features.social_accounts":":count cuentas sociales","billing.subscribe.features.workspaces":":count workspaces","billing.subscribe.features.members":":count miembros del equipo","billing.subscribe.features.credits":":count créditos IA/mes","billing.subscribe.credit_tooltips.starter":"En promedio 150 posts de largo medio + 5 imágenes IA por mes.","billing.subscribe.credit_tooltips.plus":"En promedio 300 posts de largo medio + 10 imágenes IA por mes.","billing.subscribe.credit_tooltips.pro":"En promedio 700 posts de largo medio + 30 imágenes IA por mes.","billing.subscribe.credit_tooltips.max":"En promedio 2.000 posts de largo medio + 100 imágenes IA por mes.","billing.plan.title":"Plan","billing.plan.description":"Gestiona tu plan de suscripción.","billing.plan.change":"Cambiar plan","billing.plan.label":"Plan","billing.plan.price":"Precio","billing.plan.month":"mes","billing.plan.trial":"Prueba","billing.plan.active":"Activo","billing.plan.past_due":"Vencido","billing.plan.cancelling":"Cancelando","billing.plan.trial_ends":"La prueba termina en","billing.subscription.title":"Suscripción","billing.subscription.description":"Gestiona tu método de pago, datos de facturación y suscripción.","billing.subscription.payment_method":"Método de pago","billing.subscription.no_payment_method":"Aún no hay método de pago registrado.","billing.subscription.expires_on":"Vence el :month/:year","billing.subscription.manage_label":"Suscripción","billing.subscription.manage_stripe":"Gestionar en Stripe","billing.invoices.title":"Facturas","billing.invoices.description":"Descarga tus facturas anteriores.","billing.invoices.empty":"No se encontraron facturas","billing.invoices.paid":"Pagado","billing.flash.plan_changed":"Ahora estás en el plan :plan.","billing.flash.cannot_manage":"Solo el propietario de la cuenta puede gestionar la facturación.","billing.flash.cannot_downgrade.workspaces":"No puedes cambiar a :plan: tienes :count workspaces pero el plan solo permite :limit.","billing.flash.cannot_downgrade.social_accounts":"No puedes cambiar a :plan: tienes :count cuentas sociales pero el plan solo permite :limit.","billing.flash.cannot_downgrade.members":"No puedes cambiar a :plan: tienes :count miembros (incluyendo invitaciones) pero el plan solo permite :limit.","billing.flash.credits_exhausted":"Sin créditos de IA — has usado tus :limit créditos mensuales. Mejora tu plan o espera hasta el próximo mes.","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","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","comments.today":"Hoy","comments.yesterday":"Ayer","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.confirm_modal.delete_keyword":"eliminar","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.date_range_picker.placeholder":"Elige un período","common.date_range_picker.today":"Hoy","common.date_range_picker.yesterday":"Ayer","common.date_range_picker.last_7_days":"Últimos 7 días","common.date_range_picker.last_30_days":"Últimos 30 días","common.date_range_picker.last_3_months":"Últimos 3 meses","common.date_range_picker.last_6_months":"Últimos 6 meses","common.date_range_picker.last_12_months":"Últimos 12 meses","common.date_range_picker.this_month":"Este mes","common.date_range_picker.last_month":"Mes pasado","common.date_range_picker.year_to_date":"Desde inicio del año","common.date_range_picker.last_year":"Año pasado","common.cancel":"Cancelar","common.clear":"Limpiar","common.close":"Cerrar","common.loading_more":"Cargando más...","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.no_search_results":"Ninguna etiqueta coincide con tu búsqueda","labels.try_different_search":"Prueba otra palabra clave o limpia la búsqueda.","labels.create_first_label":"Crea tu primera etiqueta","labels.table.name":"Nombre","labels.table.created_at":"Creado","labels.actions.edit":"Editar etiqueta","labels.actions.delete":"Eliminar 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.mentioned.subject":":name te mencionó en TryPost","mail.mentioned.title":":name te mencionó","mail.mentioned.intro":":name te mencionó en un comentario.","mail.mentioned.cta":"Ver comentario","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","notifications.post_ready.title":"Tu publicación está lista","notifications.post_ready.body":"La IA terminó. Toca para revisar y publicar.","notifications.account_disconnected.title":"Cuenta de :platform desconectada","notifications.account_disconnected.body":":account necesita reconectarse","notifications.account_token_expired.title":"Cuenta de :platform necesita reconectarse","notifications.account_token_expired.body":"La sesión de :account expiró — reconéctala para seguir publicando","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.no_search_results":"Ningún post coincide con tu búsqueda","posts.try_different_search":"Prueba otra palabra clave o limpia la búsqueda.","posts.start_creating":"Empieza creando tu primer post.","posts.filter_by_label":"Filtrar por etiqueta","posts.label_search_placeholder":"Buscar etiquetas...","posts.no_labels":"No se encontraron etiquetas.","posts.clear_label_filter":"Limpiar filtro de etiquetas","posts.table.post":"Post","posts.table.status":"Estado","posts.table.content":"Contenido","posts.table.platforms":"Plataformas","posts.table.labels":"Etiquetas","posts.table.scheduled_at":"Fecha","posts.table.actions":"","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","posts.actions.duplicate":"Duplicar","posts.actions.copy_id":"Copiar ID","posts.actions.copied":"ID copiado al portapapeles","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.content_exceeds_platform":":platform: demasiado largo por :over caracteres (máx :limit).","posts.form.tiktok.settings":"Configuración de TikTok","posts.form.tiktok.variant_label":"Tipo de publicación","posts.form.tiktok.variant.video":"Video","posts.form.tiktok.variant.photo":"Carrusel de fotos","posts.form.tiktok.posting_to":"Publicando en","posts.form.tiktok.privacy_level":"¿Quién puede ver este video?","posts.form.tiktok.privacy_placeholder":"Selecciona la visibilidad","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.private_disabled_branded":"El contenido de marca no puede ser privado.","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.disclose":"Divulgar contenido del video","posts.form.tiktok.disclose_hint":"Activa para divulgar que este video promueve bienes o servicios a cambio de algo de valor. Tu video puede promocionarte a ti, a un tercero o ambos.","posts.form.tiktok.promotional_organic_title":"Tu foto/video será etiquetado como \"Contenido Promocional\".","posts.form.tiktok.promotional_paid_title":"Tu foto/video será etiquetado como \"Asociación pagada\".","posts.form.tiktok.promotional_description":"Esto no se puede cambiar una vez publicado el video.","posts.form.tiktok.compliance_incomplete":"Debes indicar si tu contenido promociona a ti mismo, a un tercero o a ambos.","posts.form.tiktok.privacy_required":"La visibilidad de TikTok es obligatoria al publicar.","posts.form.tiktok.branded_cleared_private":"La visibilidad se borró porque el contenido de marca no puede ser privado.","posts.form.tiktok.interaction_disabled_by_creator":"Desactivado por la configuración de tu cuenta TikTok.","posts.form.tiktok.max_duration_exceeded":"El video dura :duration s pero esta cuenta solo permite videos de hasta :max s.","posts.form.tiktok.processing_hint":"Después de publicar, puede tardar unos minutos en procesarse y aparecer en tu perfil de TikTok.","posts.form.tiktok.brand_organic":"Tu marca","posts.form.tiktok.brand_organic_hint":"Estás promocionándote a ti mismo o a tu propia marca. Este video será clasificado como Brand Organic.","posts.form.tiktok.brand_content":"Contenido patrocinado","posts.form.tiktok.brand_content_hint":"Estás promocionando otra marca o a un tercero. Este video será clasificado como Branded Content.","posts.form.tiktok.compliance.agree":"Al publicar, aceptas la","posts.form.tiktok.compliance.music_usage":"Confirmación de Uso de Música","posts.form.tiktok.compliance.and":"y la","posts.form.tiktok.compliance.branded_policy":"Política de Contenido Patrocinado","posts.form.instagram.settings":"Configuración de Instagram","posts.form.instagram.posting_to":"Publicando en","posts.form.instagram.variant_label":"Tipo de publicación","posts.form.instagram.variant.feed":"Publicación","posts.form.instagram.variant.reel":"Reel","posts.form.instagram.variant.story":"Historia","posts.form.instagram.aspect_label":"Proporción","posts.form.instagram.aspect.square":"Cuadrado (1:1)","posts.form.instagram.aspect.portrait":"Vertical (4:5)","posts.form.instagram.aspect.landscape":"Horizontal (16:9)","posts.form.instagram.aspect.original":"Original","posts.form.facebook.settings":"Configuración de Facebook","posts.form.facebook.posting_to":"Publicando en","posts.form.facebook.variant_label":"Tipo de publicación","posts.form.facebook.variant.post":"Publicación","posts.form.facebook.variant.reel":"Reel","posts.form.facebook.variant.story":"Historia","posts.form.linkedin.settings":"Configuración de LinkedIn","posts.form.linkedin.settings_page":"Configuración de la Página de LinkedIn","posts.form.linkedin.posting_to":"Publicando en","posts.form.linkedin.variant_label":"Tipo de publicación","posts.form.linkedin.variant.post":"Publicación","posts.form.linkedin.variant.carousel":"Carrusel","posts.form.pinterest.settings":"Configuración de Pinterest","posts.form.pinterest.posting_to":"Publicando en","posts.form.pinterest.variant_label":"Tipo de pin","posts.form.pinterest.variant.pin":"Pin","posts.form.pinterest.variant.video_pin":"Video Pin","posts.form.pinterest.variant.carousel":"Carrusel","posts.form.pinterest.board":"Tablero","posts.form.pinterest.select_board":"Selecciona un tablero","posts.form.pinterest.no_boards":"No se encontraron tableros de Pinterest. Crea uno en tu cuenta de Pinterest primero.","posts.form.pinterest.search_board":"Buscar tableros...","posts.form.pinterest.no_board_found":"Ningún tablero coincide con tu búsqueda.","posts.form.pinterest.board_required":"Selecciona un tablero de Pinterest para publicar este post.","posts.form.warnings.no_variant":"Elige un tipo de publicación para continuar.","posts.form.warnings.requires_media":"Este tipo requiere al menos una imagen o video.","posts.form.warnings.max_files_exceeded":"Este tipo acepta hasta :max archivos (tienes :current).","posts.form.warnings.min_files_required":"Este tipo requiere al menos :min archivos (tienes :current).","posts.form.warnings.no_video_allowed":"Este tipo no acepta videos.","posts.form.warnings.no_image_allowed":"Este tipo acepta solo videos.","posts.form.warnings.gif_not_allowed":"Esta red no acepta GIF. Elimínalo o selecciona otra red.","posts.form.warnings.image_too_large":"La imagen supera el límite de :max (la tuya es :current).","posts.form.warnings.video_too_large":"El video supera el límite de :max (el tuyo es :current).","posts.form.warnings.video_too_long":"El video dura :current, pero este tipo permite hasta :max.","posts.form.warnings.aspect_ratio_too_narrow":"La proporción :current es demasiado alta (mínimo :min).","posts.form.warnings.aspect_ratio_too_wide":"La proporción :current es demasiado ancha (máximo :max).","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.ai.generate.button_tooltip":"Generar con IA","posts.ai.generate.title":"Generar post con IA","posts.ai.generate.description":"Describe sobre qué debe ser el post. La IA usará el contexto de tu marca para escribirlo.","posts.ai.generate.prompt_label":"¿De qué trata este post?","posts.ai.generate.prompt_placeholder":"ej: anunciar nuestra nueva función de generación de imágenes para carruseles","posts.ai.generate.preview_label":"Vista previa","posts.ai.generate.start":"Generar","posts.ai.generate.apply":"Usar este contenido","posts.ai.generate.retry":"Intentar de nuevo","posts.ai.generate.cancel":"Cancelar","posts.ai.review.button_tooltip":"Revisar con IA","posts.ai.review.title":"Revisar post con IA","posts.ai.review.description":"La IA analiza gramática, ortografía y claridad. Aplica las sugerencias con las que estés de acuerdo.","posts.ai.review.loading":"Revisando tu texto...","posts.ai.review.no_issues":"No se encontraron problemas. Todo bien.","posts.ai.review.original":"Original","posts.ai.review.suggestion":"Sugerencia","posts.ai.review.apply":"Aplicar","posts.ai.review.apply_all":"Aplicar todas","posts.ai.review.applied":"Aplicada","posts.ai.review.cancel":"Cancelar","posts.show.title":"Detalles del post","posts.show.edit":"Editar","posts.show.back":"Volver","posts.show.no_content":"Sin texto","posts.show.platforms":"Plataformas","posts.show.no_platforms":"Ninguna plataforma seleccionada.","posts.show.view_on_platform":"Ver en la plataforma","posts.show.published_on":"Publicado el :date","posts.show.scheduled_for":"Programado para el :date","posts.show.draft":"Borrador","posts.show.status_pending":"Pendiente","posts.show.metrics":"Métricas","posts.show.metrics_loading":"Cargando métricas…","posts.show.metrics_unavailable":"Métricas aún no disponibles para esta plataforma.","posts.show.metrics_empty":"No se devolvieron métricas.","posts.edit.title":"Editar post","posts.edit.view_title":"Ver post","posts.edit.labels":"Etiquetas","posts.edit.signatures":"Firmas","posts.edit.schedule":"Programar","posts.edit.delete":"Eliminar","posts.edit.schedule_for":"Programar para","posts.edit.unschedule":"Desprogramar","posts.edit.saving":"Guardando...","posts.edit.saved":"Guardado","posts.edit.draft":"Borrador","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.preview_empty.title":"Ninguna plataforma seleccionada","posts.edit.preview_empty.description":"Selecciona una plataforma para publicar y ver la vista previa.","posts.edit.drop_zone_title":"Añadir media","posts.edit.drop_zone_subtitle":"Arrastra archivos o haz clic para seleccionar","posts.edit.add":"Añadir","posts.edit.publish_to":"Publicar en","posts.edit.organize":"Organizar","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.compliance_incomplete":"Algunas configuraciones de plataforma están incompletas o son incompatibles con los medios adjuntos.","posts.edit.compliance.requires_media":"Agrega una imagen o video para publicar aquí.","posts.edit.compliance.too_many_files":"Solo se permiten :max archivo(s) en este formato.","posts.edit.compliance.too_few_files":"Agrega al menos :min archivos para este formato.","posts.edit.compliance.no_videos":"Solo se permiten imágenes en este formato.","posts.edit.compliance.no_images":"Solo se permiten videos en este formato.","posts.edit.compliance.no_gifs":"Los GIFs no son compatibles aquí.","posts.edit.compliance.video_too_large":"El video supera el límite de tamaño de esta plataforma.","posts.edit.compliance.video_too_long":"El video debe durar menos de :seconds segundos en este formato.","posts.edit.compliance.image_too_large":"La imagen supera el límite de tamaño de esta plataforma.","posts.edit.compliance.aspect_ratio_invalid":"La proporción de aspecto no es compatible con este formato.","posts.edit.compliance.no_content_type":"Elige un tipo de contenido para esta plataforma.","posts.edit.publishing":"Publicando...","posts.edit.publishing_overlay_title":"Tu publicación se está enviando","posts.edit.publishing_overlay_subtitle":"Esto puede tardar unos momentos. Puedes salir de esta página sin problemas.","posts.edit.scheduled_overlay_title":"Esta publicación está programada","posts.edit.scheduled_overlay_subtitle":"Programada para :date. Cancela la programación para hacer cambios.","posts.edit.unschedule_cta":"Cancelar para editar","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.media_picker.title":"Elegir de la galería","posts.edit.media_picker.search":"Buscar media...","posts.edit.media_picker.empty":"Aún no hay archivos en tu galería","posts.edit.media_picker.cancel":"Cancelar","posts.edit.media_picker.add":"Agregar","posts.edit.media_picker.add_count":"Agregar :count","posts.edit.emoji_picker.search":"Buscar emoji","posts.edit.emoji_picker.empty":"No se encontraron emojis","posts.edit.emoji_picker.recent":"Usados con frecuencia","posts.edit.emoji_picker.smileys":"Caritas y emociones","posts.edit.emoji_picker.people":"Personas y cuerpo","posts.edit.emoji_picker.nature":"Animales y naturaleza","posts.edit.emoji_picker.food":"Comida y bebida","posts.edit.emoji_picker.activities":"Actividades","posts.edit.emoji_picker.travel":"Viajes y lugares","posts.edit.emoji_picker.objects":"Objetos","posts.edit.emoji_picker.symbols":"Símbolos","posts.edit.emoji_picker.flags":"Banderas","posts.edit.status.scheduled":"Programado","posts.edit.status.published":"Publicado","posts.edit.status.publishing":"Publicando...","posts.edit.status.failed":"Fallido","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.signatures_modal.search":"Buscar firmas...","posts.edit.signatures_modal.no_results":"No se encontraron firmas.","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.tiktok_photo.label":"Carrusel de fotos","posts.content_types.tiktok_photo.description":"Hasta 35 fotos en un carrusel deslizable","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 estándar","posts.content_types.pinterest_video_pin.label":"Pin de video","posts.content_types.pinterest_video_pin.description":"Pin de video (4s - 15min)","posts.content_types.pinterest_carousel.label":"Carrusel","posts.content_types.pinterest_carousel.description":"Carrusel multi-imagen (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.deleted":"¡Post eliminado correctamente!","posts.flash.duplicated":"Post duplicado como borrador.","posts.flash.cannot_edit_published":"Los posts publicados no se pueden editar.","posts.flash.cannot_delete_published":"Los posts publicados no se pueden eliminar.","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","posts.errors.account_token_expired":"Sesión de la cuenta social expirada — reconecta la cuenta","posts.delete.title":"¿Eliminar post?","posts.delete.description":"Esta acción no se puede deshacer. El post y todos sus archivos multimedia se eliminarán de forma permanente.","posts.delete.confirm":"Sí, eliminar","posts.delete.cancel":"Cancelar","posts.create.title":"Crear nuevo post","posts.create.description":"Elige cómo quieres empezar.","posts.create.scratch_title":"Empezar desde cero","posts.create.scratch_description":"Abre un post en blanco para escribirlo todo.","posts.create.ai_title":"Generar con IA","posts.create.ai_description":"Describe lo que quieres y la IA genera el contenido por ti.","posts.create.ai_configure_description":"Elige un formato y describe el post que quieres crear.","posts.create.template_title":"Usar una plantilla","posts.create.template_description":"Elige una de nuestras plantillas y personalízala.","posts.create.preview.image_title":"Título de la imagen","posts.create.preview.image_body":"Texto de la imagen","posts.create.coming_soon":"Próximamente","posts.create.steps.format_title":"Elige un formato","posts.create.steps.format_description":"Selecciona el tipo de post que quieres crear.","posts.create.steps.account_title":"Elige una cuenta","posts.create.steps.account_description":"Selecciona la cuenta social donde publicar.","posts.create.steps.media_title":"Opciones de medios","posts.create.steps.media_carousel":"¿Cuántas diapositivas?","posts.create.steps.media_optional":"¿Incluir imágenes?","posts.create.steps.media_optional_label":"¿Cuántas imágenes?","posts.create.steps.media_none":"Ninguna","posts.create.steps.media_count_label":"Número de imágenes","posts.create.steps.prompt_title":"Describe tu post","posts.create.steps.prompt_label":"¿De qué trata este post?","posts.create.steps.prompt_placeholder":"Ej. Anuncia nuestra nueva función de carrusel para Instagram","posts.create.steps.preview_error":"Algo salió mal. Por favor, inténtalo de nuevo.","posts.create.steps.loading_page_title":"Generando tu publicación","posts.create.steps.loading_eta":"Tiempo estimado: cerca de :minutes.","posts.create.steps.loading_eta_minute_one":"1 minuto","posts.create.steps.loading_eta_minute_other":":count minutos","posts.create.steps.loading_leave_title":"Puedes seguir trabajando.","posts.create.steps.loading_leave_body":"Te avisamos cuando la publicación esté lista.","posts.create.steps.loading_leave_cta":"Ir al calendario","posts.create.steps.loading_create_another_cta":"Crear otra publicación","posts.create.steps.loading_tip_credits":"Cada imagen IA usa unos 15 créditos.","posts.create.steps.loading_tip_edit":"Podrás editar todo cuando la publicación esté lista.","posts.create.steps.loading_tip_draft":"Las publicaciones generadas van directo a tus borradores.","posts.create.steps.loading_tip_brand":"Ajusta tu marca para influir en las próximas publicaciones.","posts.create.steps.loading_tip_carousel":"Los carruseles generan una diapositiva por imagen solicitada.","posts.create.steps.loading_tip_quality":"La calidad balancea velocidad y costo.","posts.create.steps.create":"Crear post","posts.create.steps.back":"Atrás","posts.create.steps.next":"Continuar","posts.create.steps.cancel":"Cancelar","posts.create.steps.discard":"Descartar","posts.create.steps.retry":"Intentar de nuevo","posts.create.steps.no_platforms":"Sin cuentas conectadas","posts.create.steps.connect_first":"Conecta al menos una cuenta social para usar la generación con IA.","posts.create.steps.format.instagram_feed":"Post de Feed de Instagram","posts.create.steps.format.instagram_carousel":"Carrusel de Instagram","posts.create.steps.format.linkedin_post":"Post de LinkedIn","posts.create.steps.format.linkedin_page_post":"Post de Página de LinkedIn","posts.create.steps.format.x_post":"Post en X","posts.create.steps.format.bluesky_post":"Post en Bluesky","posts.create.steps.format.threads_post":"Post en Threads","posts.create.steps.format.mastodon_post":"Post en Mastodon","posts.create.steps.format.facebook_post":"Post en Facebook","posts.create.steps.format.pinterest_pin":"Pin de Pinterest","posts.create.steps.format.instagram_story":"Story de Instagram","posts.create.steps.format.facebook_story":"Story de Facebook","posts.templates.browser_title":"Elige una plantilla","posts.templates.browser_description":"Comienza con una plantilla curada y adáptala.","posts.templates.search_placeholder":"Buscar plantillas…","posts.templates.no_search_results":"Ninguna plantilla coincide con tu búsqueda","posts.templates.try_different_search":"Prueba otra palabra clave o limpia la búsqueda.","posts.templates.slides_count":"{count} slide|{count} slides","posts.templates.all_platforms":"Todas las plataformas","posts.templates.platform_search_placeholder":"Buscar plataforma…","posts.templates.no_platform_match":"Ninguna plataforma coincide.","posts.templates.use_this":"Usar esta plantilla","posts.templates.no_templates":"No hay plantillas disponibles.","posts.templates.applying":"Aplicando plantilla…","posts.templates.category.product_launch":"Lanzamiento de producto","posts.templates.category.promotion":"Promoción","posts.templates.category.educational":"Educativo","posts.templates.category.behind_the_scenes":"Detrás de cámaras","posts.templates.category.testimonial":"Testimonio","posts.templates.category.industry_tip":"Consejo del sector","posts.templates.category.event":"Evento","posts.templates.category.engagement":"Interacción","settings.title":"Configuración","settings.description":"Administra tu perfil y configuración de la cuenta","settings.hub.title":"Configuración","settings.hub.description":"Elige qué quieres gestionar.","settings.hub.profile.title":"Perfil","settings.hub.profile.description":"Actualiza tu información personal, contraseña y preferencias de notificaciones.","settings.hub.workspace.title":"Workspace","settings.hub.workspace.description":"Configura tu workspace, marca, miembros y claves de API.","settings.hub.account.title":"Cuenta","settings.hub.account.description":"Gestiona la información de la cuenta, uso y facturación.","settings.nav.profile":"Perfil","settings.nav.authentication":"Autenticación","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.authentication.title":"Autenticación","settings.authentication.page_title":"Configuración de autenticación","settings.authentication.sessions.title":"Sesiones activas","settings.authentication.sessions.description":"Si notas algo sospechoso, cierra sesión en otros dispositivos.","settings.authentication.sessions.unknown_browser":"Navegador desconocido","settings.authentication.sessions.unknown_ip":"IP desconocida","settings.authentication.sessions.on":"en","settings.authentication.sessions.active_now":"Activa ahora","settings.authentication.sessions.log_out_others":"Cerrar otras sesiones","settings.authentication.sessions.modal_title":"Cerrar otras sesiones","settings.authentication.sessions.modal_description_password":"Introduce tu contraseña actual para confirmar el cierre de las demás sesiones.","settings.authentication.sessions.modal_description_email":"Escribe tu correo electrónico para confirmar el cierre de las demás sesiones.","settings.authentication.sessions.password_placeholder":"Contraseña actual","settings.authentication.sessions.email_placeholder":"Tu correo","settings.authentication.sessions.cancel":"Cancelar","settings.authentication.sessions.submit":"Cerrar otras sesiones","settings.authentication.sessions.email_mismatch":"El correo electrónico no coincide con tu cuenta.","settings.authentication.sessions.flash_logged_out":"Has cerrado sesión en los demás dispositivos.","settings.authentication.password.update_title":"Actualizar contraseña","settings.authentication.password.set_title":"Definir una contraseña","settings.authentication.password.update_description":"Asegúrate de usar una contraseña larga y aleatoria para mantener tu cuenta segura.","settings.authentication.password.set_description":"Añade una contraseña para iniciar sesión sin un proveedor conectado.","settings.authentication.password.current_password":"Contraseña actual","settings.authentication.password.new_password":"Nueva contraseña","settings.authentication.password.confirm_password":"Confirmar contraseña","settings.authentication.password.save":"Guardar contraseña","settings.authentication.password.set":"Definir contraseña","settings.authentication.providers.title":"Cuentas conectadas","settings.authentication.providers.description":"Inicia sesión más rápido con estos proveedores conectados.","settings.authentication.providers.connected":"Conectada","settings.authentication.providers.not_connected":"No conectada","settings.authentication.providers.connect":"Conectar","settings.authentication.providers.disconnect":"Desconectar","settings.authentication.providers.flash_disconnected":":provider desconectada correctamente.","settings.authentication.providers.flash_connected":":provider conectada correctamente.","settings.authentication.providers.flash_already_linked":"Esa cuenta de :provider ya está vinculada a otro usuario.","settings.authentication.providers.flash_cannot_disconnect":"No puedes desconectar tu único método de inicio de sesión. Define una contraseña o conecta otro proveedor primero.","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_password":"Una vez eliminada, todos sus recursos y datos también se eliminarán permanentemente. Introduce tu contraseña para confirmar.","settings.delete_account.modal_description_email":"Una vez eliminada, todos sus recursos y datos también se eliminarán permanentemente. Escribe tu correo :email para confirmar.","settings.delete_account.password":"Contraseña","settings.delete_account.password_placeholder":"Contraseña","settings.delete_account.email_placeholder":"Tu correo","settings.delete_account.email_mismatch":"El correo electrónico no coincide con tu cuenta.","settings.delete_account.cancel":"Cancelar","settings.delete_account.confirm":"Eliminar cuenta","settings.workspace.tabs.workspace":"Workspace","settings.workspace.tabs.brand":"Marca","settings.workspace.tabs.users":"Miembros","settings.workspace.tabs.api_keys":"API Keys","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.name":"Nombre del workspace","settings.brand.name_placeholder":"Mi marca","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.brand_color":"Color de marca","settings.brand.background_color":"Color de fondo","settings.brand.text_color":"Color de texto","settings.brand.font":"Fuente","settings.brand.image_style":"Estilo de imágenes","settings.brand.image_style_description":"Estilo visual aplicado al generar imágenes de diapositivas y portadas para publicaciones con IA.","settings.brand.image_style_cinematic":"Cinematográfico","settings.brand.image_style_illustration":"Ilustración","settings.brand.image_style_isometric_3d":"Isométrico","settings.brand.image_style_cartoon":"Cartoon","settings.brand.image_style_typographic":"Tipográfico","settings.brand.image_style_infographic":"Infográfico","settings.brand.image_style_minimalist":"Minimalista","settings.brand.image_style_mockup":"Mockup","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.roles.viewer":"Espectador","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.account.tabs.account":"Cuenta","settings.account.tabs.usage":"Uso","settings.account.tabs.billing":"Facturación","settings.account.title":"Configuración de cuenta","settings.account.description":"Gestiona el nombre de la cuenta y el correo de facturación","settings.account.name":"Nombre de la cuenta","settings.account.name_placeholder":"Mi Empresa","settings.account.billing_email":"Correo de facturación","settings.account.billing_email_placeholder":"facturacion@empresa.com","settings.account.billing_email_hint":"Este correo se usará para facturas y comunicaciones de facturación de Stripe.","settings.account.submit":"Guardar","settings.flash.account_updated":"¡Cuenta actualizada correctamente!","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.signatures":"Firmas","sidebar.workspace.labels":"Etiquetas","sidebar.workspace.assets":"Medios","sidebar.workspace.api_keys":"API Keys","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.support":"Soporte","sidebar.analytics":"Analytics","sidebar.settings":"Configuración","sidebar.posts.calendar":"Calendario","sidebar.posts.all":"Todos","sidebar.posts.scheduled":"Programados","sidebar.posts.posted":"Publicados","sidebar.posts.drafts":"Borradores","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","signatures.title":"Firmas","signatures.description":"Crea firmas reutilizables para añadir rápidamente a tus posts","signatures.search":"Buscar firmas...","signatures.new":"Nueva firma","signatures.empty_title":"Aún no hay firmas","signatures.empty_description":"Crea firmas para añadir hashtags, links o cualquier texto reutilizable a tus posts","signatures.no_search_results":"Ninguna firma coincide con tu búsqueda","signatures.try_different_search":"Prueba otra palabra clave o limpia la búsqueda.","signatures.table.name":"Nombre","signatures.table.content":"Contenido","signatures.table.created_at":"Creado","signatures.actions.edit":"Editar firma","signatures.actions.delete":"Eliminar firma","signatures.create.title":"Crear firma","signatures.create.description":"Dale un nombre a tu firma y el contenido para añadir (hashtags, links, texto libre — lo que reutilizas).","signatures.create.name":"Nombre","signatures.create.name_placeholder":"ej: Marketing, Viaje, Cierre de marca","signatures.create.content":"Contenido","signatures.create.content_placeholder":"#marketing #socialmedia\nMás info: https://tumarca.com","signatures.create.content_hint":"Hashtags, links, intros, cierres — cualquier cosa que añades a los posts.","signatures.create.submit":"Crear firma","signatures.create.submitting":"Creando...","signatures.edit.title":"Editar firma","signatures.edit.description":"Actualiza el nombre y el contenido de esta firma.","signatures.edit.name":"Nombre","signatures.edit.name_placeholder":"ej: Marketing, Viaje, Cierre de marca","signatures.edit.content":"Contenido","signatures.edit.content_placeholder":"#marketing #socialmedia\nMás info: https://tumarca.com","signatures.edit.content_hint":"Hashtags, links, intros, cierres — cualquier cosa que añades a los posts.","signatures.edit.submit":"Guardar cambios","signatures.edit.submitting":"Guardando...","signatures.delete.title":"Eliminar firma","signatures.delete.description":"¿Seguro que quieres eliminar esta firma? Esta acción no se puede deshacer.","signatures.delete.confirm":"Eliminar","signatures.delete.cancel":"Cancelar","signatures.flash.created":"Firma creada.","signatures.flash.updated":"Firma actualizada.","signatures.flash.deleted":"Firma eliminada.","usage.title":"Uso","usage.section_account":"Cuenta","usage.section_account_description":"Cuotas y límites de tu plan :plan.","usage.section_ai":"Créditos AI","usage.section_ai_description":"Los créditos se debitan a medida que usas las funciones de AI. Se renuevan el día 1 de cada mes.","usage.workspaces":"Workspaces","usage.social_accounts":"Cuentas Sociales","usage.members":"Miembros","usage.credits":"Créditos","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 un poco sobre ti o tu proyecto. 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.brand_color":"Color de marca","workspaces.create.background_color":"Color de fondo","workspaces.create.text_color":"Color de texto","workspaces.create.submit":"Crear workspace","workspaces.create.success":"Workspace creado. Conecta una cuenta social para empezar a publicar.","workspaces.limit_reached":"Has alcanzado el límite de workspaces de tu plan.","workspaces.flash.deleted":"Workspace eliminado correctamente."} \ No newline at end of file +{"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.connect_cta":"Conectar","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.no_search_results":"Ninguna cuenta coincide con tu búsqueda","accounts.try_different_search":"Prueba otra palabra clave o limpia la búsqueda.","accounts.search":"Buscar cuentas...","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.table.account":"Cuenta","accounts.table.platform":"Plataforma","accounts.table.status":"Estado","accounts.table.last_used":"Último uso","accounts.table.added":"Añadida","accounts.table.active":"Activa","accounts.never_used":"Nunca usada","accounts.status.connected":"Conectada","accounts.status.disconnected":"Desconectada","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.","accounts.popup_callback.title_success":"Conectado","accounts.popup_callback.title_error":"Error","accounts.popup_callback.closing":"Esta ventana se cerrará automáticamente...","accounts.popup_callback.close_now":"Puedes cerrar esta ventana ahora.","accounts.popup_callback.connected":"¡Cuenta conectada!","accounts.popup_callback.reconnected":"¡Cuenta reconectada!","accounts.popup_callback.error_connecting":"Error al conectar la cuenta. Inténtalo de nuevo.","accounts.popup_callback.error_connecting_page":"Error al conectar la página. Inténtalo de nuevo.","accounts.popup_callback.error_connecting_channel":"Error al conectar el canal. Inténtalo de nuevo.","accounts.popup_callback.session_expired":"Sesión expirada. Inténtalo de nuevo.","accounts.popup_callback.workspace_not_found":"Workspace no encontrado.","accounts.popup_callback.invalid_state":"Estado inválido. Inténtalo de nuevo.","accounts.popup_callback.failed_to_authenticate":"Falló la autenticación.","accounts.popup_callback.failed_to_get_profile":"Falló al obtener el perfil.","accounts.popup_callback.page_not_found":"Página no encontrada.","accounts.popup_callback.channel_not_found":"Canal no encontrado.","accounts.popup_callback.no_facebook_pages":"No se encontraron páginas de Facebook. Debes ser administrador de al menos una página.","accounts.popup_callback.no_facebook_instagram_pages":"No se encontraron páginas de Facebook con cuentas de Instagram vinculadas.","accounts.popup_callback.no_youtube_channels":"No se encontraron canales de YouTube. Crea un canal primero.","accounts.popup_callback.not_linkedin_admin":"No eres administrador de ninguna página de LinkedIn.","analytics.no_accounts":"No hay cuentas conectadas con analytics.","analytics.no_accounts_match":"Ninguna cuenta coincide.","analytics.search_account":"Buscar cuenta…","analytics.select_account":"Selecciona una cuenta para ver analytics.","analytics.no_data":"No hay datos de analytics disponibles.","analytics.metrics.avg_view_duration":"Duración Media (s)","analytics.metrics.avg_view_percentage":"Porcentaje Medio de Visualización","analytics.metrics.bookmarks":"Guardados","analytics.metrics.clicks":"Clics","analytics.metrics.comments":"Comentarios","analytics.metrics.engagement":"Engagement","analytics.metrics.favourites":"Favoritos","analytics.metrics.followers":"Seguidores","analytics.metrics.following":"Siguiendo","analytics.metrics.impressions":"Impresiones","analytics.metrics.interactions":"Interacciones","analytics.metrics.likes":"Me gusta","analytics.metrics.minutes_watched":"Minutos Vistos","analytics.metrics.organic_followers":"Seguidores Orgánicos","analytics.metrics.outbound_clicks":"Clics Externos","analytics.metrics.page_followers":"Seguidores de la Página","analytics.metrics.page_reach":"Alcance de la Página","analytics.metrics.page_views":"Vistas de la Página","analytics.metrics.paid_followers":"Seguidores Pagados","analytics.metrics.pin_click_rate":"Tasa de Clics en Pines","analytics.metrics.pin_clicks":"Clics en Pines","analytics.metrics.posts_engagement":"Engagement de Publicaciones","analytics.metrics.posts_reach":"Alcance de Publicaciones","analytics.metrics.quotes":"Citas","analytics.metrics.reach":"Alcance","analytics.metrics.reblogs":"Reblogs","analytics.metrics.recent_comments":"Comentarios Recientes","analytics.metrics.recent_likes":"Me Gusta Recientes","analytics.metrics.recent_shares":"Compartidos Recientes","analytics.metrics.replies":"Respuestas","analytics.metrics.reposts":"Reposts","analytics.metrics.retweets":"Retweets","analytics.metrics.saves":"Guardados","analytics.metrics.shares":"Compartidos","analytics.metrics.subscribers_gained":"Suscriptores Ganados","analytics.metrics.subscribers_lost":"Suscriptores Perdidos","analytics.metrics.total_likes":"Total de Me gusta","analytics.metrics.video_views":"Vistas de Vídeo","analytics.metrics.videos":"Vídeos","analytics.metrics.views":"Vistas","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.add_to_post":"Agregar al post","assets.search_placeholder":"Buscar media...","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","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.signatures.title":"Firmas","auth.slides.signatures.description":"Guarda firmas reutilizables (hashtags, links, despedidas) y añádelas a tus posts con un clic.","auth.or_continue_with":"O continuar con","auth.google_login":"Iniciar sesión con Google","auth.google_signup":"Registrarse con Google","auth.github_login":"Iniciar sesión con GitHub","auth.github_signup":"Registrarse con GitHub","auth.github_email_unavailable":"No fue posible obtener tu correo de GitHub. Haz tu correo público en GitHub o concede el permiso de correo y vuelve a intentar.","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":"Facturación","billing.upgrade_dialog.title":"Actualiza tu plan","billing.upgrade_dialog.description":"Elige un plan que se adapte a tus necesidades.","billing.upgrade_dialog.current_plan":"Plan actual","billing.upgrade_dialog.current_short":"Actual","billing.upgrade_dialog.current_badge":"Actual","billing.upgrade_dialog.subscribe":"Suscribirse","billing.upgrade_dialog.switch":"Cambiar a este plan","billing.upgrade_dialog.switch_short":"Cambiar","billing.upgrade_dialog.switch_to_yearly":"Cambiar a anual","billing.upgrade_dialog.switch_to_monthly":"Cambiar a mensual","billing.upgrade_dialog.unavailable":"No disponible","billing.upgrade_dialog.reasons.workspace_limit":"Has alcanzado el límite de workspaces de tu plan. Actualiza para crear más.","billing.upgrade_dialog.reasons.social_account_limit":"Has alcanzado el límite de cuentas sociales de tu plan. Actualiza para conectar más.","billing.upgrade_dialog.reasons.member_limit":"Has alcanzado el límite de miembros de tu plan. Actualiza para invitar a más personas.","billing.subscribe.page_title":"Elige tu plan","billing.subscribe.eyebrow":"Precios","billing.subscribe.title":"Elige el plan ideal para ti","billing.subscribe.description":"Elige el plan que te queda. Facturación mensual o anual.","billing.subscribe.monthly":"Mensual","billing.subscribe.yearly":"Anual","billing.subscribe.per_month":"mensual","billing.subscribe.per_year":"anual","billing.subscribe.billed_monthly":"Facturado mensualmente","billing.subscribe.billed_yearly":"Facturado anualmente","billing.subscribe.features_included":"Qué incluye:","billing.subscribe.everything_in":"Todo lo de :plan, más:","billing.subscribe.save_months":"2 meses gratis","billing.subscribe.popular":"Más popular","billing.subscribe.subscribe_cta":"Suscribirse","billing.subscribe.prices.starter.monthly":"$19","billing.subscribe.prices.starter.yearly_per_month":"$16","billing.subscribe.prices.starter.yearly":"$190","billing.subscribe.prices.plus.monthly":"$29","billing.subscribe.prices.plus.yearly_per_month":"$24","billing.subscribe.prices.plus.yearly":"$290","billing.subscribe.prices.pro.monthly":"$49","billing.subscribe.prices.pro.yearly_per_month":"$41","billing.subscribe.prices.pro.yearly":"$490","billing.subscribe.prices.max.monthly":"$99","billing.subscribe.prices.max.yearly_per_month":"$83","billing.subscribe.prices.max.yearly":"$990","billing.subscribe.features.social_accounts":":count cuentas sociales","billing.subscribe.features.workspaces":":count workspaces","billing.subscribe.features.members":":count miembros del equipo","billing.subscribe.features.credits":":count créditos IA/mes","billing.subscribe.credit_tooltips.starter":"En promedio 150 posts de largo medio + 5 imágenes IA por mes.","billing.subscribe.credit_tooltips.plus":"En promedio 300 posts de largo medio + 10 imágenes IA por mes.","billing.subscribe.credit_tooltips.pro":"En promedio 700 posts de largo medio + 30 imágenes IA por mes.","billing.subscribe.credit_tooltips.max":"En promedio 2.000 posts de largo medio + 100 imágenes IA por mes.","billing.plan.title":"Plan","billing.plan.description":"Gestiona tu plan de suscripción.","billing.plan.change":"Cambiar plan","billing.plan.label":"Plan","billing.plan.price":"Precio","billing.plan.month":"mes","billing.plan.trial":"Prueba","billing.plan.active":"Activo","billing.plan.past_due":"Vencido","billing.plan.cancelling":"Cancelando","billing.plan.trial_ends":"La prueba termina en","billing.subscription.title":"Suscripción","billing.subscription.description":"Gestiona tu método de pago, datos de facturación y suscripción.","billing.subscription.payment_method":"Método de pago","billing.subscription.no_payment_method":"Aún no hay método de pago registrado.","billing.subscription.expires_on":"Vence el :month/:year","billing.subscription.manage_label":"Suscripción","billing.subscription.manage_stripe":"Gestionar en Stripe","billing.invoices.title":"Facturas","billing.invoices.description":"Descarga tus facturas anteriores.","billing.invoices.empty":"No se encontraron facturas","billing.invoices.paid":"Pagado","billing.flash.plan_changed":"Ahora estás en el plan :plan.","billing.flash.cannot_manage":"Solo el propietario de la cuenta puede gestionar la facturación.","billing.flash.cannot_downgrade.workspaces":"No puedes cambiar a :plan: tienes :count workspaces pero el plan solo permite :limit.","billing.flash.cannot_downgrade.social_accounts":"No puedes cambiar a :plan: tienes :count cuentas sociales pero el plan solo permite :limit.","billing.flash.cannot_downgrade.members":"No puedes cambiar a :plan: tienes :count miembros (incluyendo invitaciones) pero el plan solo permite :limit.","billing.flash.credits_exhausted":"Sin créditos de IA — has usado tus :limit créditos mensuales. Mejora tu plan o espera hasta el próximo mes.","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","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","comments.today":"Hoy","comments.yesterday":"Ayer","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.confirm_modal.delete_keyword":"eliminar","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.date_range_picker.placeholder":"Elige un período","common.date_range_picker.today":"Hoy","common.date_range_picker.yesterday":"Ayer","common.date_range_picker.last_7_days":"Últimos 7 días","common.date_range_picker.last_30_days":"Últimos 30 días","common.date_range_picker.last_3_months":"Últimos 3 meses","common.date_range_picker.last_6_months":"Últimos 6 meses","common.date_range_picker.last_12_months":"Últimos 12 meses","common.date_range_picker.this_month":"Este mes","common.date_range_picker.last_month":"Mes pasado","common.date_range_picker.year_to_date":"Desde inicio del año","common.date_range_picker.last_year":"Año pasado","common.cancel":"Cancelar","common.clear":"Limpiar","common.close":"Cerrar","common.loading_more":"Cargando más...","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.no_search_results":"Ninguna etiqueta coincide con tu búsqueda","labels.try_different_search":"Prueba otra palabra clave o limpia la búsqueda.","labels.create_first_label":"Crea tu primera etiqueta","labels.table.name":"Nombre","labels.table.created_at":"Creado","labels.actions.edit":"Editar etiqueta","labels.actions.delete":"Eliminar 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.mentioned.subject":":name te mencionó en TryPost","mail.mentioned.title":":name te mencionó","mail.mentioned.intro":":name te mencionó en un comentario.","mail.mentioned.cta":"Ver comentario","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","notifications.post_ready.title":"Tu publicación está lista","notifications.post_ready.body":"La IA terminó. Toca para revisar y publicar.","notifications.account_disconnected.title":"Cuenta de :platform desconectada","notifications.account_disconnected.body":":account necesita reconectarse","notifications.account_token_expired.title":"Cuenta de :platform necesita reconectarse","notifications.account_token_expired.body":"La sesión de :account expiró — reconéctala para seguir publicando","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.no_search_results":"Ningún post coincide con tu búsqueda","posts.try_different_search":"Prueba otra palabra clave o limpia la búsqueda.","posts.start_creating":"Empieza creando tu primer post.","posts.filter_by_label":"Filtrar por etiqueta","posts.label_search_placeholder":"Buscar etiquetas...","posts.no_labels":"No se encontraron etiquetas.","posts.clear_label_filter":"Limpiar filtro de etiquetas","posts.table.post":"Post","posts.table.status":"Estado","posts.table.content":"Contenido","posts.table.platforms":"Plataformas","posts.table.labels":"Etiquetas","posts.table.scheduled_at":"Fecha","posts.table.actions":"","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","posts.actions.duplicate":"Duplicar","posts.actions.copy_id":"Copiar ID","posts.actions.copied":"ID copiado al portapapeles","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.content_exceeds_platform":":platform: demasiado largo por :over caracteres (máx :limit).","posts.form.tiktok.settings":"Configuración de TikTok","posts.form.tiktok.variant_label":"Tipo de publicación","posts.form.tiktok.variant.video":"Video","posts.form.tiktok.variant.photo":"Carrusel de fotos","posts.form.tiktok.posting_to":"Publicando en","posts.form.tiktok.privacy_level":"¿Quién puede ver este video?","posts.form.tiktok.privacy_placeholder":"Selecciona la visibilidad","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.private_disabled_branded":"El contenido de marca no puede ser privado.","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.disclose":"Divulgar contenido del video","posts.form.tiktok.disclose_hint":"Activa para divulgar que este video promueve bienes o servicios a cambio de algo de valor. Tu video puede promocionarte a ti, a un tercero o ambos.","posts.form.tiktok.promotional_organic_title":"Tu foto/video será etiquetado como \"Contenido Promocional\".","posts.form.tiktok.promotional_paid_title":"Tu foto/video será etiquetado como \"Asociación pagada\".","posts.form.tiktok.promotional_description":"Esto no se puede cambiar una vez publicado el video.","posts.form.tiktok.compliance_incomplete":"Debes indicar si tu contenido promociona a ti mismo, a un tercero o a ambos.","posts.form.tiktok.privacy_required":"La visibilidad de TikTok es obligatoria al publicar.","posts.form.tiktok.branded_cleared_private":"La visibilidad se borró porque el contenido de marca no puede ser privado.","posts.form.tiktok.interaction_disabled_by_creator":"Desactivado por la configuración de tu cuenta TikTok.","posts.form.tiktok.max_duration_exceeded":"El video dura :duration s pero esta cuenta solo permite videos de hasta :max s.","posts.form.tiktok.processing_hint":"Después de publicar, puede tardar unos minutos en procesarse y aparecer en tu perfil de TikTok.","posts.form.tiktok.brand_organic":"Tu marca","posts.form.tiktok.brand_organic_hint":"Estás promocionándote a ti mismo o a tu propia marca. Este video será clasificado como Brand Organic.","posts.form.tiktok.brand_content":"Contenido patrocinado","posts.form.tiktok.brand_content_hint":"Estás promocionando otra marca o a un tercero. Este video será clasificado como Branded Content.","posts.form.tiktok.compliance.agree":"Al publicar, aceptas la","posts.form.tiktok.compliance.music_usage":"Confirmación de Uso de Música","posts.form.tiktok.compliance.and":"y la","posts.form.tiktok.compliance.branded_policy":"Política de Contenido Patrocinado","posts.form.instagram.settings":"Configuración de Instagram","posts.form.instagram.posting_to":"Publicando en","posts.form.instagram.variant_label":"Tipo de publicación","posts.form.instagram.variant.feed":"Publicación","posts.form.instagram.variant.reel":"Reel","posts.form.instagram.variant.story":"Historia","posts.form.instagram.aspect_label":"Proporción","posts.form.instagram.aspect.square":"Cuadrado (1:1)","posts.form.instagram.aspect.portrait":"Vertical (4:5)","posts.form.instagram.aspect.landscape":"Horizontal (16:9)","posts.form.instagram.aspect.original":"Original","posts.form.facebook.settings":"Configuración de Facebook","posts.form.facebook.posting_to":"Publicando en","posts.form.facebook.variant_label":"Tipo de publicación","posts.form.facebook.variant.post":"Publicación","posts.form.facebook.variant.reel":"Reel","posts.form.facebook.variant.story":"Historia","posts.form.linkedin.settings":"Configuración de LinkedIn","posts.form.linkedin.settings_page":"Configuración de la Página de LinkedIn","posts.form.linkedin.posting_to":"Publicando en","posts.form.linkedin.variant_label":"Tipo de publicación","posts.form.linkedin.variant.post":"Publicación","posts.form.linkedin.variant.carousel":"Carrusel","posts.form.pinterest.settings":"Configuración de Pinterest","posts.form.pinterest.posting_to":"Publicando en","posts.form.pinterest.variant_label":"Tipo de pin","posts.form.pinterest.variant.pin":"Pin","posts.form.pinterest.variant.video_pin":"Video Pin","posts.form.pinterest.variant.carousel":"Carrusel","posts.form.pinterest.board":"Tablero","posts.form.pinterest.select_board":"Selecciona un tablero","posts.form.pinterest.no_boards":"No se encontraron tableros de Pinterest. Crea uno en tu cuenta de Pinterest primero.","posts.form.pinterest.search_board":"Buscar tableros...","posts.form.pinterest.no_board_found":"Ningún tablero coincide con tu búsqueda.","posts.form.pinterest.board_required":"Selecciona un tablero de Pinterest para publicar este post.","posts.form.warnings.no_variant":"Elige un tipo de publicación para continuar.","posts.form.warnings.requires_media":"Este tipo requiere al menos una imagen o video.","posts.form.warnings.max_files_exceeded":"Este tipo acepta hasta :max archivos (tienes :current).","posts.form.warnings.min_files_required":"Este tipo requiere al menos :min archivos (tienes :current).","posts.form.warnings.no_video_allowed":"Este tipo no acepta videos.","posts.form.warnings.no_image_allowed":"Este tipo acepta solo videos.","posts.form.warnings.gif_not_allowed":"Esta red no acepta GIF. Elimínalo o selecciona otra red.","posts.form.warnings.image_too_large":"La imagen supera el límite de :max (la tuya es :current).","posts.form.warnings.video_too_large":"El video supera el límite de :max (el tuyo es :current).","posts.form.warnings.video_too_long":"El video dura :current, pero este tipo permite hasta :max.","posts.form.warnings.aspect_ratio_too_narrow":"La proporción :current es demasiado alta (mínimo :min).","posts.form.warnings.aspect_ratio_too_wide":"La proporción :current es demasiado ancha (máximo :max).","posts.status.pending":"Pendiente","posts.status.draft":"Borrador","posts.status.scheduled":"Programado","posts.status.publishing":"Publicando","posts.status.retrying":"Reintentando","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.ai.generate.button_tooltip":"Generar con IA","posts.ai.generate.title":"Generar post con IA","posts.ai.generate.description":"Describe sobre qué debe ser el post. La IA usará el contexto de tu marca para escribirlo.","posts.ai.generate.prompt_label":"¿De qué trata este post?","posts.ai.generate.prompt_placeholder":"ej: anunciar nuestra nueva función de generación de imágenes para carruseles","posts.ai.generate.preview_label":"Vista previa","posts.ai.generate.start":"Generar","posts.ai.generate.apply":"Usar este contenido","posts.ai.generate.retry":"Intentar de nuevo","posts.ai.generate.cancel":"Cancelar","posts.ai.review.button_tooltip":"Revisar con IA","posts.ai.review.title":"Revisar post con IA","posts.ai.review.description":"La IA analiza gramática, ortografía y claridad. Aplica las sugerencias con las que estés de acuerdo.","posts.ai.review.loading":"Revisando tu texto...","posts.ai.review.no_issues":"No se encontraron problemas. Todo bien.","posts.ai.review.original":"Original","posts.ai.review.suggestion":"Sugerencia","posts.ai.review.apply":"Aplicar","posts.ai.review.apply_all":"Aplicar todas","posts.ai.review.applied":"Aplicada","posts.ai.review.cancel":"Cancelar","posts.show.title":"Detalles del post","posts.show.edit":"Editar","posts.show.back":"Volver","posts.show.no_content":"Sin texto","posts.show.platforms":"Plataformas","posts.show.no_platforms":"Ninguna plataforma seleccionada.","posts.show.view_on_platform":"Ver en la plataforma","posts.show.published_on":"Publicado el :date","posts.show.scheduled_for":"Programado para el :date","posts.show.draft":"Borrador","posts.show.status_pending":"Pendiente","posts.show.metrics":"Métricas","posts.show.metrics_loading":"Cargando métricas…","posts.show.metrics_unavailable":"Métricas aún no disponibles para esta plataforma.","posts.show.metrics_empty":"No se devolvieron métricas.","posts.edit.title":"Editar post","posts.edit.view_title":"Ver post","posts.edit.labels":"Etiquetas","posts.edit.signatures":"Firmas","posts.edit.schedule":"Programar","posts.edit.delete":"Eliminar","posts.edit.schedule_for":"Programar para","posts.edit.unschedule":"Desprogramar","posts.edit.saving":"Guardando...","posts.edit.saved":"Guardado","posts.edit.draft":"Borrador","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.preview_empty.title":"Ninguna plataforma seleccionada","posts.edit.preview_empty.description":"Selecciona una plataforma para publicar y ver la vista previa.","posts.edit.drop_zone_title":"Añadir media","posts.edit.drop_zone_subtitle":"Arrastra archivos o haz clic para seleccionar","posts.edit.add":"Añadir","posts.edit.publish_to":"Publicar en","posts.edit.organize":"Organizar","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.compliance_incomplete":"Algunas configuraciones de plataforma están incompletas o son incompatibles con los medios adjuntos.","posts.edit.compliance.requires_media":"Agrega una imagen o video para publicar aquí.","posts.edit.compliance.too_many_files":"Solo se permiten :max archivo(s) en este formato.","posts.edit.compliance.too_few_files":"Agrega al menos :min archivos para este formato.","posts.edit.compliance.no_videos":"Solo se permiten imágenes en este formato.","posts.edit.compliance.no_images":"Solo se permiten videos en este formato.","posts.edit.compliance.no_gifs":"Los GIFs no son compatibles aquí.","posts.edit.compliance.video_too_large":"El video supera el límite de tamaño de esta plataforma.","posts.edit.compliance.video_too_long":"El video debe durar menos de :seconds segundos en este formato.","posts.edit.compliance.image_too_large":"La imagen supera el límite de tamaño de esta plataforma.","posts.edit.compliance.aspect_ratio_invalid":"La proporción de aspecto no es compatible con este formato.","posts.edit.compliance.no_content_type":"Elige un tipo de contenido para esta plataforma.","posts.edit.publishing":"Publicando...","posts.edit.publishing_overlay_title":"Tu publicación se está enviando","posts.edit.publishing_overlay_subtitle":"Esto puede tardar unos momentos. Puedes salir de esta página sin problemas.","posts.edit.scheduled_overlay_title":"Esta publicación está programada","posts.edit.scheduled_overlay_subtitle":"Programada para :date. Cancela la programación para hacer cambios.","posts.edit.unschedule_cta":"Cancelar para editar","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.media_picker.title":"Elegir de la galería","posts.edit.media_picker.search":"Buscar media...","posts.edit.media_picker.empty":"Aún no hay archivos en tu galería","posts.edit.media_picker.cancel":"Cancelar","posts.edit.media_picker.add":"Agregar","posts.edit.media_picker.add_count":"Agregar :count","posts.edit.emoji_picker.search":"Buscar emoji","posts.edit.emoji_picker.empty":"No se encontraron emojis","posts.edit.emoji_picker.recent":"Usados con frecuencia","posts.edit.emoji_picker.smileys":"Caritas y emociones","posts.edit.emoji_picker.people":"Personas y cuerpo","posts.edit.emoji_picker.nature":"Animales y naturaleza","posts.edit.emoji_picker.food":"Comida y bebida","posts.edit.emoji_picker.activities":"Actividades","posts.edit.emoji_picker.travel":"Viajes y lugares","posts.edit.emoji_picker.objects":"Objetos","posts.edit.emoji_picker.symbols":"Símbolos","posts.edit.emoji_picker.flags":"Banderas","posts.edit.status.scheduled":"Programado","posts.edit.status.published":"Publicado","posts.edit.status.publishing":"Publicando...","posts.edit.status.retrying":"Reintentando...","posts.edit.status.failed":"Fallido","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.signatures_modal.search":"Buscar firmas...","posts.edit.signatures_modal.no_results":"No se encontraron firmas.","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.tiktok_photo.label":"Carrusel de fotos","posts.content_types.tiktok_photo.description":"Hasta 35 fotos en un carrusel deslizable","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 estándar","posts.content_types.pinterest_video_pin.label":"Pin de video","posts.content_types.pinterest_video_pin.description":"Pin de video (4s - 15min)","posts.content_types.pinterest_carousel.label":"Carrusel","posts.content_types.pinterest_carousel.description":"Carrusel multi-imagen (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.deleted":"¡Post eliminado correctamente!","posts.flash.duplicated":"Post duplicado como borrador.","posts.flash.cannot_edit_published":"Los posts publicados no se pueden editar.","posts.flash.cannot_delete_published":"Los posts publicados no se pueden eliminar.","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","posts.errors.account_token_expired":"Sesión de la cuenta social expirada — reconecta la cuenta","posts.delete.title":"¿Eliminar post?","posts.delete.description":"Esta acción no se puede deshacer. El post y todos sus archivos multimedia se eliminarán de forma permanente.","posts.delete.confirm":"Sí, eliminar","posts.delete.cancel":"Cancelar","posts.create.title":"Crear nuevo post","posts.create.description":"Elige cómo quieres empezar.","posts.create.scratch_title":"Empezar desde cero","posts.create.scratch_description":"Abre un post en blanco para escribirlo todo.","posts.create.ai_title":"Generar con IA","posts.create.ai_description":"Describe lo que quieres y la IA genera el contenido por ti.","posts.create.ai_configure_description":"Elige un formato y describe el post que quieres crear.","posts.create.template_title":"Usar una plantilla","posts.create.template_description":"Elige una de nuestras plantillas y personalízala.","posts.create.preview.image_title":"Título de la imagen","posts.create.preview.image_body":"Texto de la imagen","posts.create.coming_soon":"Próximamente","posts.create.steps.format_title":"Elige un formato","posts.create.steps.format_description":"Selecciona el tipo de post que quieres crear.","posts.create.steps.account_title":"Elige una cuenta","posts.create.steps.account_description":"Selecciona la cuenta social donde publicar.","posts.create.steps.media_title":"Opciones de medios","posts.create.steps.media_carousel":"¿Cuántas diapositivas?","posts.create.steps.media_optional":"¿Incluir imágenes?","posts.create.steps.media_optional_label":"¿Cuántas imágenes?","posts.create.steps.media_none":"Ninguna","posts.create.steps.media_count_label":"Número de imágenes","posts.create.steps.prompt_title":"Describe tu post","posts.create.steps.prompt_label":"¿De qué trata este post?","posts.create.steps.prompt_placeholder":"Ej. Anuncia nuestra nueva función de carrusel para Instagram","posts.create.steps.preview_error":"Algo salió mal. Por favor, inténtalo de nuevo.","posts.create.steps.loading_page_title":"Generando tu publicación","posts.create.steps.loading_eta":"Tiempo estimado: cerca de :minutes.","posts.create.steps.loading_eta_minute_one":"1 minuto","posts.create.steps.loading_eta_minute_other":":count minutos","posts.create.steps.loading_leave_title":"Puedes seguir trabajando.","posts.create.steps.loading_leave_body":"Te avisamos cuando la publicación esté lista.","posts.create.steps.loading_leave_cta":"Ir al calendario","posts.create.steps.loading_create_another_cta":"Crear otra publicación","posts.create.steps.loading_tip_credits":"Cada imagen IA usa unos 15 créditos.","posts.create.steps.loading_tip_edit":"Podrás editar todo cuando la publicación esté lista.","posts.create.steps.loading_tip_draft":"Las publicaciones generadas van directo a tus borradores.","posts.create.steps.loading_tip_brand":"Ajusta tu marca para influir en las próximas publicaciones.","posts.create.steps.loading_tip_carousel":"Los carruseles generan una diapositiva por imagen solicitada.","posts.create.steps.loading_tip_quality":"La calidad balancea velocidad y costo.","posts.create.steps.create":"Crear post","posts.create.steps.back":"Atrás","posts.create.steps.next":"Continuar","posts.create.steps.cancel":"Cancelar","posts.create.steps.discard":"Descartar","posts.create.steps.retry":"Intentar de nuevo","posts.create.steps.no_platforms":"Sin cuentas conectadas","posts.create.steps.connect_first":"Conecta al menos una cuenta social para usar la generación con IA.","posts.create.steps.format.instagram_feed":"Post de Feed de Instagram","posts.create.steps.format.instagram_carousel":"Carrusel de Instagram","posts.create.steps.format.linkedin_post":"Post de LinkedIn","posts.create.steps.format.linkedin_page_post":"Post de Página de LinkedIn","posts.create.steps.format.x_post":"Post en X","posts.create.steps.format.bluesky_post":"Post en Bluesky","posts.create.steps.format.threads_post":"Post en Threads","posts.create.steps.format.mastodon_post":"Post en Mastodon","posts.create.steps.format.facebook_post":"Post en Facebook","posts.create.steps.format.pinterest_pin":"Pin de Pinterest","posts.create.steps.format.instagram_story":"Story de Instagram","posts.create.steps.format.facebook_story":"Story de Facebook","posts.templates.browser_title":"Elige una plantilla","posts.templates.browser_description":"Comienza con una plantilla curada y adáptala.","posts.templates.search_placeholder":"Buscar plantillas…","posts.templates.no_search_results":"Ninguna plantilla coincide con tu búsqueda","posts.templates.try_different_search":"Prueba otra palabra clave o limpia la búsqueda.","posts.templates.slides_count":"{count} slide|{count} slides","posts.templates.all_platforms":"Todas las plataformas","posts.templates.platform_search_placeholder":"Buscar plataforma…","posts.templates.no_platform_match":"Ninguna plataforma coincide.","posts.templates.use_this":"Usar esta plantilla","posts.templates.no_templates":"No hay plantillas disponibles.","posts.templates.applying":"Aplicando plantilla…","posts.templates.category.product_launch":"Lanzamiento de producto","posts.templates.category.promotion":"Promoción","posts.templates.category.educational":"Educativo","posts.templates.category.behind_the_scenes":"Detrás de cámaras","posts.templates.category.testimonial":"Testimonio","posts.templates.category.industry_tip":"Consejo del sector","posts.templates.category.event":"Evento","posts.templates.category.engagement":"Interacción","settings.title":"Configuración","settings.description":"Administra tu perfil y configuración de la cuenta","settings.hub.title":"Configuración","settings.hub.description":"Elige qué quieres gestionar.","settings.hub.profile.title":"Perfil","settings.hub.profile.description":"Actualiza tu información personal, contraseña y preferencias de notificaciones.","settings.hub.workspace.title":"Workspace","settings.hub.workspace.description":"Configura tu workspace, marca, miembros y claves de API.","settings.hub.account.title":"Cuenta","settings.hub.account.description":"Gestiona la información de la cuenta, uso y facturación.","settings.nav.profile":"Perfil","settings.nav.authentication":"Autenticación","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.authentication.title":"Autenticación","settings.authentication.page_title":"Configuración de autenticación","settings.authentication.sessions.title":"Sesiones activas","settings.authentication.sessions.description":"Si notas algo sospechoso, cierra sesión en otros dispositivos.","settings.authentication.sessions.unknown_browser":"Navegador desconocido","settings.authentication.sessions.unknown_ip":"IP desconocida","settings.authentication.sessions.on":"en","settings.authentication.sessions.active_now":"Activa ahora","settings.authentication.sessions.log_out_others":"Cerrar otras sesiones","settings.authentication.sessions.modal_title":"Cerrar otras sesiones","settings.authentication.sessions.modal_description_password":"Introduce tu contraseña actual para confirmar el cierre de las demás sesiones.","settings.authentication.sessions.modal_description_email":"Escribe tu correo electrónico para confirmar el cierre de las demás sesiones.","settings.authentication.sessions.password_placeholder":"Contraseña actual","settings.authentication.sessions.email_placeholder":"Tu correo","settings.authentication.sessions.cancel":"Cancelar","settings.authentication.sessions.submit":"Cerrar otras sesiones","settings.authentication.sessions.email_mismatch":"El correo electrónico no coincide con tu cuenta.","settings.authentication.sessions.flash_logged_out":"Has cerrado sesión en los demás dispositivos.","settings.authentication.password.update_title":"Actualizar contraseña","settings.authentication.password.set_title":"Definir una contraseña","settings.authentication.password.update_description":"Asegúrate de usar una contraseña larga y aleatoria para mantener tu cuenta segura.","settings.authentication.password.set_description":"Añade una contraseña para iniciar sesión sin un proveedor conectado.","settings.authentication.password.current_password":"Contraseña actual","settings.authentication.password.new_password":"Nueva contraseña","settings.authentication.password.confirm_password":"Confirmar contraseña","settings.authentication.password.save":"Guardar contraseña","settings.authentication.password.set":"Definir contraseña","settings.authentication.providers.title":"Cuentas conectadas","settings.authentication.providers.description":"Inicia sesión más rápido con estos proveedores conectados.","settings.authentication.providers.connected":"Conectada","settings.authentication.providers.not_connected":"No conectada","settings.authentication.providers.connect":"Conectar","settings.authentication.providers.disconnect":"Desconectar","settings.authentication.providers.flash_disconnected":":provider desconectada correctamente.","settings.authentication.providers.flash_connected":":provider conectada correctamente.","settings.authentication.providers.flash_already_linked":"Esa cuenta de :provider ya está vinculada a otro usuario.","settings.authentication.providers.flash_cannot_disconnect":"No puedes desconectar tu único método de inicio de sesión. Define una contraseña o conecta otro proveedor primero.","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_password":"Una vez eliminada, todos sus recursos y datos también se eliminarán permanentemente. Introduce tu contraseña para confirmar.","settings.delete_account.modal_description_email":"Una vez eliminada, todos sus recursos y datos también se eliminarán permanentemente. Escribe tu correo :email para confirmar.","settings.delete_account.password":"Contraseña","settings.delete_account.password_placeholder":"Contraseña","settings.delete_account.email_placeholder":"Tu correo","settings.delete_account.email_mismatch":"El correo electrónico no coincide con tu cuenta.","settings.delete_account.cancel":"Cancelar","settings.delete_account.confirm":"Eliminar cuenta","settings.workspace.tabs.workspace":"Workspace","settings.workspace.tabs.brand":"Marca","settings.workspace.tabs.users":"Miembros","settings.workspace.tabs.api_keys":"API Keys","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.name":"Nombre del workspace","settings.brand.name_placeholder":"Mi marca","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.brand_color":"Color de marca","settings.brand.background_color":"Color de fondo","settings.brand.text_color":"Color de texto","settings.brand.font":"Fuente","settings.brand.image_style":"Estilo de imágenes","settings.brand.image_style_description":"Estilo visual aplicado al generar imágenes de diapositivas y portadas para publicaciones con IA.","settings.brand.image_style_cinematic":"Cinematográfico","settings.brand.image_style_illustration":"Ilustración","settings.brand.image_style_isometric_3d":"Isométrico","settings.brand.image_style_cartoon":"Cartoon","settings.brand.image_style_typographic":"Tipográfico","settings.brand.image_style_infographic":"Infográfico","settings.brand.image_style_minimalist":"Minimalista","settings.brand.image_style_mockup":"Mockup","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.roles.viewer":"Espectador","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.account.tabs.account":"Cuenta","settings.account.tabs.usage":"Uso","settings.account.tabs.billing":"Facturación","settings.account.title":"Configuración de cuenta","settings.account.description":"Gestiona el nombre de la cuenta y el correo de facturación","settings.account.name":"Nombre de la cuenta","settings.account.name_placeholder":"Mi Empresa","settings.account.billing_email":"Correo de facturación","settings.account.billing_email_placeholder":"facturacion@empresa.com","settings.account.billing_email_hint":"Este correo se usará para facturas y comunicaciones de facturación de Stripe.","settings.account.submit":"Guardar","settings.flash.account_updated":"¡Cuenta actualizada correctamente!","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.signatures":"Firmas","sidebar.workspace.labels":"Etiquetas","sidebar.workspace.assets":"Medios","sidebar.workspace.api_keys":"API Keys","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.support":"Soporte","sidebar.analytics":"Analytics","sidebar.settings":"Configuración","sidebar.posts.calendar":"Calendario","sidebar.posts.all":"Todos","sidebar.posts.scheduled":"Programados","sidebar.posts.posted":"Publicados","sidebar.posts.drafts":"Borradores","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","signatures.title":"Firmas","signatures.description":"Crea firmas reutilizables para añadir rápidamente a tus posts","signatures.search":"Buscar firmas...","signatures.new":"Nueva firma","signatures.empty_title":"Aún no hay firmas","signatures.empty_description":"Crea firmas para añadir hashtags, links o cualquier texto reutilizable a tus posts","signatures.no_search_results":"Ninguna firma coincide con tu búsqueda","signatures.try_different_search":"Prueba otra palabra clave o limpia la búsqueda.","signatures.table.name":"Nombre","signatures.table.content":"Contenido","signatures.table.created_at":"Creado","signatures.actions.edit":"Editar firma","signatures.actions.delete":"Eliminar firma","signatures.create.title":"Crear firma","signatures.create.description":"Dale un nombre a tu firma y el contenido para añadir (hashtags, links, texto libre — lo que reutilizas).","signatures.create.name":"Nombre","signatures.create.name_placeholder":"ej: Marketing, Viaje, Cierre de marca","signatures.create.content":"Contenido","signatures.create.content_placeholder":"#marketing #socialmedia\nMás info: https://tumarca.com","signatures.create.content_hint":"Hashtags, links, intros, cierres — cualquier cosa que añades a los posts.","signatures.create.submit":"Crear firma","signatures.create.submitting":"Creando...","signatures.edit.title":"Editar firma","signatures.edit.description":"Actualiza el nombre y el contenido de esta firma.","signatures.edit.name":"Nombre","signatures.edit.name_placeholder":"ej: Marketing, Viaje, Cierre de marca","signatures.edit.content":"Contenido","signatures.edit.content_placeholder":"#marketing #socialmedia\nMás info: https://tumarca.com","signatures.edit.content_hint":"Hashtags, links, intros, cierres — cualquier cosa que añades a los posts.","signatures.edit.submit":"Guardar cambios","signatures.edit.submitting":"Guardando...","signatures.delete.title":"Eliminar firma","signatures.delete.description":"¿Seguro que quieres eliminar esta firma? Esta acción no se puede deshacer.","signatures.delete.confirm":"Eliminar","signatures.delete.cancel":"Cancelar","signatures.flash.created":"Firma creada.","signatures.flash.updated":"Firma actualizada.","signatures.flash.deleted":"Firma eliminada.","usage.title":"Uso","usage.section_account":"Cuenta","usage.section_account_description":"Cuotas y límites de tu plan :plan.","usage.section_ai":"Créditos AI","usage.section_ai_description":"Los créditos se debitan a medida que usas las funciones de AI. Se renuevan el día 1 de cada mes.","usage.workspaces":"Workspaces","usage.social_accounts":"Cuentas Sociales","usage.members":"Miembros","usage.credits":"Créditos","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 un poco sobre ti o tu proyecto. 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.brand_color":"Color de marca","workspaces.create.background_color":"Color de fondo","workspaces.create.text_color":"Color de texto","workspaces.create.submit":"Crear workspace","workspaces.create.success":"Workspace creado. Conecta una cuenta social para empezar a publicar.","workspaces.limit_reached":"Has alcanzado el límite de workspaces de tu plan.","workspaces.flash.deleted":"Workspace eliminado correctamente."} \ No newline at end of file diff --git a/lang/php_pt-BR.json b/lang/php_pt-BR.json index c93dfe24..9d575455 100644 --- a/lang/php_pt-BR.json +++ b/lang/php_pt-BR.json @@ -1 +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.connect_cta":"Conectar","accounts.no_accounts":"Nenhuma conta conectada ainda","accounts.no_accounts_description":"Conecte suas redes sociais para começar a agendar e publicar posts","accounts.no_search_results":"Nenhuma conta corresponde à sua busca","accounts.try_different_search":"Tente outra palavra-chave ou limpe a busca.","accounts.search":"Buscar contas...","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.table.account":"Conta","accounts.table.platform":"Plataforma","accounts.table.status":"Status","accounts.table.last_used":"Último uso","accounts.table.added":"Adicionada","accounts.table.active":"Ativa","accounts.never_used":"Nunca usada","accounts.status.connected":"Conectada","accounts.status.disconnected":"Desconectada","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.","accounts.popup_callback.title_success":"Conectado","accounts.popup_callback.title_error":"Erro","accounts.popup_callback.closing":"Esta janela será fechada automaticamente...","accounts.popup_callback.close_now":"Você pode fechar esta janela agora.","accounts.popup_callback.connected":"Conta conectada!","accounts.popup_callback.reconnected":"Conta reconectada!","accounts.popup_callback.error_connecting":"Erro ao conectar conta. Por favor, tente novamente.","accounts.popup_callback.error_connecting_page":"Erro ao conectar página. Por favor, tente novamente.","accounts.popup_callback.error_connecting_channel":"Erro ao conectar canal. Por favor, tente novamente.","accounts.popup_callback.session_expired":"Sessão expirada. Por favor, tente novamente.","accounts.popup_callback.workspace_not_found":"Workspace não encontrado.","accounts.popup_callback.invalid_state":"Estado inválido. Por favor, tente novamente.","accounts.popup_callback.failed_to_authenticate":"Falha na autenticação.","accounts.popup_callback.failed_to_get_profile":"Falha ao obter perfil.","accounts.popup_callback.page_not_found":"Página não encontrada.","accounts.popup_callback.channel_not_found":"Canal não encontrado.","accounts.popup_callback.no_facebook_pages":"Nenhuma página do Facebook encontrada. Você precisa ser administrador de pelo menos uma página.","accounts.popup_callback.no_facebook_instagram_pages":"Nenhuma página do Facebook com conta do Instagram vinculada foi encontrada.","accounts.popup_callback.no_youtube_channels":"Nenhum canal do YouTube encontrado. Por favor, crie um canal primeiro.","accounts.popup_callback.not_linkedin_admin":"Você não é administrador de nenhuma página do LinkedIn.","analytics.no_accounts":"Nenhuma conta conectada com analytics.","analytics.no_accounts_match":"Nenhuma conta corresponde.","analytics.search_account":"Buscar conta…","analytics.select_account":"Selecione uma conta para ver analytics.","analytics.no_data":"Nenhum dado de analytics disponível.","analytics.metrics.avg_view_duration":"Duração Média (s)","analytics.metrics.avg_view_percentage":"Visualização Média","analytics.metrics.bookmarks":"Salvos","analytics.metrics.clicks":"Cliques","analytics.metrics.comments":"Comentários","analytics.metrics.engagement":"Engajamento","analytics.metrics.favourites":"Favoritos","analytics.metrics.followers":"Seguidores","analytics.metrics.following":"Seguindo","analytics.metrics.impressions":"Impressões","analytics.metrics.interactions":"Interações","analytics.metrics.likes":"Curtidas","analytics.metrics.minutes_watched":"Minutos Assistidos","analytics.metrics.organic_followers":"Seguidores Orgânicos","analytics.metrics.outbound_clicks":"Cliques Externos","analytics.metrics.page_followers":"Seguidores da Página","analytics.metrics.page_reach":"Alcance da Página","analytics.metrics.page_views":"Visualizações da Página","analytics.metrics.paid_followers":"Seguidores Pagos","analytics.metrics.pin_click_rate":"Taxa de Clique em Pins","analytics.metrics.pin_clicks":"Cliques em Pins","analytics.metrics.posts_engagement":"Engajamento dos Posts","analytics.metrics.posts_reach":"Alcance dos Posts","analytics.metrics.quotes":"Citações","analytics.metrics.reach":"Alcance","analytics.metrics.reblogs":"Reblogs","analytics.metrics.recent_comments":"Comentários Recentes","analytics.metrics.recent_likes":"Curtidas Recentes","analytics.metrics.recent_shares":"Compartilhamentos Recentes","analytics.metrics.replies":"Respostas","analytics.metrics.reposts":"Reposts","analytics.metrics.retweets":"Retweets","analytics.metrics.saves":"Salvos","analytics.metrics.shares":"Compartilhamentos","analytics.metrics.subscribers_gained":"Inscritos Ganhos","analytics.metrics.subscribers_lost":"Inscritos Perdidos","analytics.metrics.total_likes":"Curtidas Totais","analytics.metrics.video_views":"Visualizações de Vídeo","analytics.metrics.videos":"Vídeos","analytics.metrics.views":"Visualizações","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.add_to_post":"Adicionar ao post","assets.search_placeholder":"Buscar mídia...","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","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.signatures.title":"Assinaturas","auth.slides.signatures.description":"Salve assinaturas reutilizáveis (hashtags, links, encerramentos) e anexe nos posts com um clique.","auth.or_continue_with":"Ou continue com","auth.google_login":"Entrar com Google","auth.google_signup":"Cadastrar com Google","auth.github_login":"Entrar com GitHub","auth.github_signup":"Cadastrar com GitHub","auth.github_email_unavailable":"Não foi possível obter seu e-mail do GitHub. Torne seu e-mail público ou conceda a permissão de e-mail e tente novamente.","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":"Faturamento","billing.upgrade_dialog.title":"Faça upgrade do seu plano","billing.upgrade_dialog.description":"Escolha um plano que se encaixe nas suas necessidades.","billing.upgrade_dialog.current_plan":"Plano atual","billing.upgrade_dialog.current_short":"Atual","billing.upgrade_dialog.current_badge":"Atual","billing.upgrade_dialog.subscribe":"Assinar","billing.upgrade_dialog.switch":"Mudar para este plano","billing.upgrade_dialog.switch_short":"Mudar","billing.upgrade_dialog.switch_to_yearly":"Mudar para anual","billing.upgrade_dialog.switch_to_monthly":"Mudar para mensal","billing.upgrade_dialog.unavailable":"Indisponível","billing.upgrade_dialog.reasons.workspace_limit":"Você atingiu o limite de workspaces do seu plano. Faça upgrade pra criar mais.","billing.upgrade_dialog.reasons.social_account_limit":"Você atingiu o limite de contas sociais do seu plano. Faça upgrade pra conectar mais.","billing.upgrade_dialog.reasons.member_limit":"Você atingiu o limite de membros do seu plano. Faça upgrade pra convidar mais pessoas.","billing.subscribe.page_title":"Escolha seu plano","billing.subscribe.eyebrow":"Preços","billing.subscribe.title":"Escolha o plano ideal pra você","billing.subscribe.description":"Escolha o plano que combina com você. Cobrança mensal ou anual.","billing.subscribe.monthly":"Mensal","billing.subscribe.yearly":"Anual","billing.subscribe.per_month":"mensal","billing.subscribe.per_year":"anual","billing.subscribe.billed_monthly":"Cobrança mensal","billing.subscribe.billed_yearly":"Cobrança anual","billing.subscribe.features_included":"O que está incluído:","billing.subscribe.everything_in":"Tudo do :plan, mais:","billing.subscribe.save_months":"2 meses grátis","billing.subscribe.popular":"Mais popular","billing.subscribe.subscribe_cta":"Assinar","billing.subscribe.prices.starter.monthly":"R$ 95","billing.subscribe.prices.starter.yearly_per_month":"R$ 79","billing.subscribe.prices.starter.yearly":"R$ 950","billing.subscribe.prices.plus.monthly":"R$ 145","billing.subscribe.prices.plus.yearly_per_month":"R$ 121","billing.subscribe.prices.plus.yearly":"R$ 1450","billing.subscribe.prices.pro.monthly":"R$ 245","billing.subscribe.prices.pro.yearly_per_month":"R$ 204","billing.subscribe.prices.pro.yearly":"R$ 2450","billing.subscribe.prices.max.monthly":"R$ 495","billing.subscribe.prices.max.yearly_per_month":"R$ 413","billing.subscribe.prices.max.yearly":"R$ 4950","billing.subscribe.features.social_accounts":":count contas sociais","billing.subscribe.features.workspaces":":count workspaces","billing.subscribe.features.members":":count membros da equipe","billing.subscribe.features.credits":":count créditos IA/mês","billing.subscribe.credit_tooltips.starter":"Em média 150 posts de tamanho médio + 5 imagens de IA por mês.","billing.subscribe.credit_tooltips.plus":"Em média 300 posts de tamanho médio + 10 imagens de IA por mês.","billing.subscribe.credit_tooltips.pro":"Em média 700 posts de tamanho médio + 30 imagens de IA por mês.","billing.subscribe.credit_tooltips.max":"Em média 2.000 posts de tamanho médio + 100 imagens de IA por mês.","billing.plan.title":"Plano","billing.plan.description":"Gerencie seu plano de assinatura.","billing.plan.change":"Mudar plano","billing.plan.label":"Plano","billing.plan.price":"Preço","billing.plan.month":"mês","billing.plan.trial":"Trial","billing.plan.active":"Ativo","billing.plan.past_due":"Vencido","billing.plan.cancelling":"Cancelando","billing.plan.trial_ends":"Teste termina em","billing.subscription.title":"Assinatura","billing.subscription.description":"Gerencie seu método de pagamento, dados de cobrança e assinatura.","billing.subscription.payment_method":"Método de pagamento","billing.subscription.no_payment_method":"Nenhum método de pagamento cadastrado.","billing.subscription.expires_on":"Expira em :month/:year","billing.subscription.manage_label":"Assinatura","billing.subscription.manage_stripe":"Gerenciar no Stripe","billing.invoices.title":"Faturas","billing.invoices.description":"Baixe suas faturas anteriores.","billing.invoices.empty":"Nenhuma fatura encontrada","billing.invoices.paid":"Pago","billing.flash.plan_changed":"Você está agora no plano :plan.","billing.flash.cannot_manage":"Apenas o owner da conta pode gerenciar a cobrança.","billing.flash.cannot_downgrade.workspaces":"Não é possível mudar para :plan: você tem :count workspaces mas o plano só permite :limit.","billing.flash.cannot_downgrade.social_accounts":"Não é possível mudar para :plan: você tem :count contas sociais mas o plano só permite :limit.","billing.flash.cannot_downgrade.members":"Não é possível mudar para :plan: você tem :count membros (incluindo convites) mas o plano só permite :limit.","billing.flash.credits_exhausted":"Sem créditos de IA — você usou seus :limit créditos mensais. Faça upgrade do plano ou aguarde até o próximo mês.","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","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","comments.today":"Hoje","comments.yesterday":"Ontem","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.confirm_modal.delete_keyword":"deletar","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.date_range_picker.placeholder":"Escolha um período","common.date_range_picker.today":"Hoje","common.date_range_picker.yesterday":"Ontem","common.date_range_picker.last_7_days":"Últimos 7 dias","common.date_range_picker.last_30_days":"Últimos 30 dias","common.date_range_picker.last_3_months":"Últimos 3 meses","common.date_range_picker.last_6_months":"Últimos 6 meses","common.date_range_picker.last_12_months":"Últimos 12 meses","common.date_range_picker.this_month":"Este mês","common.date_range_picker.last_month":"Mês passado","common.date_range_picker.year_to_date":"Desde o início do ano","common.date_range_picker.last_year":"Ano passado","common.cancel":"Cancelar","common.clear":"Limpar","common.close":"Fechar","common.loading_more":"Carregando mais...","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.no_search_results":"Nenhuma etiqueta corresponde à sua busca","labels.try_different_search":"Tente outra palavra-chave ou limpe a busca.","labels.create_first_label":"Crie sua primeira etiqueta","labels.table.name":"Nome","labels.table.created_at":"Criado","labels.actions.edit":"Editar etiqueta","labels.actions.delete":"Excluir 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.mentioned.subject":":name mencionou você no TryPost","mail.mentioned.title":":name mencionou você","mail.mentioned.intro":":name mencionou você num comentário.","mail.mentioned.cta":"Ver comentário","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","notifications.post_ready.title":"Seu post está pronto","notifications.post_ready.body":"A AI terminou. Toque pra revisar e publicar.","notifications.account_disconnected.title":"Conta do :platform desconectada","notifications.account_disconnected.body":":account precisa ser reconectada","notifications.account_token_expired.title":"Conta do :platform precisa ser reconectada","notifications.account_token_expired.body":"Sessão de :account expirou — reconecte pra continuar postando","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.no_search_results":"Nenhum post corresponde à sua busca","posts.try_different_search":"Tente outra palavra-chave ou limpe a busca.","posts.start_creating":"Comece criando seu primeiro post.","posts.filter_by_label":"Filtrar por label","posts.label_search_placeholder":"Buscar labels...","posts.no_labels":"Nenhuma label encontrada.","posts.clear_label_filter":"Limpar filtro de labels","posts.table.post":"Post","posts.table.status":"Status","posts.table.content":"Conteúdo","posts.table.platforms":"Plataformas","posts.table.labels":"Etiquetas","posts.table.scheduled_at":"Data","posts.table.actions":"","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","posts.actions.duplicate":"Duplicar","posts.actions.copy_id":"Copiar ID","posts.actions.copied":"ID copiado para a área de transferência","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.content_exceeds_platform":":platform: longo demais por :over caracteres (máx :limit).","posts.form.tiktok.settings":"Configurações do TikTok","posts.form.tiktok.variant_label":"Tipo de publicação","posts.form.tiktok.variant.video":"Vídeo","posts.form.tiktok.variant.photo":"Carrossel de fotos","posts.form.tiktok.posting_to":"Publicando em","posts.form.tiktok.privacy_level":"Quem pode ver este vídeo?","posts.form.tiktok.privacy_placeholder":"Selecione a visibilidade","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.private_disabled_branded":"Conteúdo de marca não pode ser privado.","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.disclose":"Divulgar conteúdo do vídeo","posts.form.tiktok.disclose_hint":"Ative para divulgar que este vídeo promove bens ou serviços em troca de algo de valor. Seu vídeo pode promover você, terceiros ou ambos.","posts.form.tiktok.promotional_organic_title":"Sua foto/vídeo será rotulado como \"Conteúdo Promocional\".","posts.form.tiktok.promotional_paid_title":"Sua foto/vídeo será rotulado como \"Parceria paga\".","posts.form.tiktok.promotional_description":"Isso não poderá ser alterado após a publicação.","posts.form.tiktok.compliance_incomplete":"Você precisa indicar se o conteúdo promove você mesmo, terceiros ou ambos.","posts.form.tiktok.privacy_required":"A visibilidade do TikTok é obrigatória ao publicar.","posts.form.tiktok.branded_cleared_private":"A visibilidade foi limpa porque conteúdo de marca não pode ser privado.","posts.form.tiktok.interaction_disabled_by_creator":"Desativado pelas configurações da sua conta TikTok.","posts.form.tiktok.max_duration_exceeded":"Vídeo tem :duration s mas esta conta só permite vídeos de até :max s.","posts.form.tiktok.processing_hint":"Após publicar, pode levar alguns minutos para o conteúdo ser processado e aparecer no seu perfil TikTok.","posts.form.tiktok.brand_organic":"Sua marca","posts.form.tiktok.brand_organic_hint":"Você está promovendo você mesmo ou sua própria marca. Este vídeo será classificado como Brand Organic.","posts.form.tiktok.brand_content":"Conteúdo patrocinado","posts.form.tiktok.brand_content_hint":"Você está promovendo outra marca ou terceiros. Este vídeo será classificado como Branded Content.","posts.form.tiktok.compliance.agree":"Ao publicar, você concorda com a","posts.form.tiktok.compliance.music_usage":"Confirmação de Uso de Música","posts.form.tiktok.compliance.and":"e","posts.form.tiktok.compliance.branded_policy":"Política de Conteúdo Patrocinado","posts.form.instagram.settings":"Configurações do Instagram","posts.form.instagram.posting_to":"Publicando em","posts.form.instagram.variant_label":"Tipo de publicação","posts.form.instagram.variant.feed":"Post","posts.form.instagram.variant.reel":"Reel","posts.form.instagram.variant.story":"Story","posts.form.instagram.aspect_label":"Proporção","posts.form.instagram.aspect.square":"Quadrado (1:1)","posts.form.instagram.aspect.portrait":"Retrato (4:5)","posts.form.instagram.aspect.landscape":"Paisagem (16:9)","posts.form.instagram.aspect.original":"Original","posts.form.facebook.settings":"Configurações do Facebook","posts.form.facebook.posting_to":"Publicando em","posts.form.facebook.variant_label":"Tipo de publicação","posts.form.facebook.variant.post":"Post","posts.form.facebook.variant.reel":"Reel","posts.form.facebook.variant.story":"Story","posts.form.linkedin.settings":"Configurações do LinkedIn","posts.form.linkedin.settings_page":"Configurações da Página do LinkedIn","posts.form.linkedin.posting_to":"Publicando em","posts.form.linkedin.variant_label":"Tipo de publicação","posts.form.linkedin.variant.post":"Post","posts.form.linkedin.variant.carousel":"Carrossel","posts.form.pinterest.settings":"Configurações do Pinterest","posts.form.pinterest.posting_to":"Publicando em","posts.form.pinterest.variant_label":"Tipo de pin","posts.form.pinterest.variant.pin":"Pin","posts.form.pinterest.variant.video_pin":"Video Pin","posts.form.pinterest.variant.carousel":"Carrossel","posts.form.pinterest.board":"Quadro","posts.form.pinterest.select_board":"Selecione um quadro","posts.form.pinterest.no_boards":"Nenhum quadro do Pinterest encontrado. Crie um na sua conta do Pinterest primeiro.","posts.form.pinterest.search_board":"Pesquisar quadros...","posts.form.pinterest.no_board_found":"Nenhum quadro encontrado.","posts.form.pinterest.board_required":"Selecione um quadro do Pinterest para publicar este post.","posts.form.warnings.no_variant":"Escolha um tipo de publicação para continuar.","posts.form.warnings.requires_media":"Este tipo exige pelo menos uma imagem ou vídeo.","posts.form.warnings.max_files_exceeded":"Este tipo aceita até :max arquivos (você tem :current).","posts.form.warnings.min_files_required":"Este tipo exige pelo menos :min arquivos (você tem :current).","posts.form.warnings.no_video_allowed":"Este tipo não aceita vídeos.","posts.form.warnings.no_image_allowed":"Este tipo aceita apenas vídeos.","posts.form.warnings.gif_not_allowed":"Esta rede não aceita GIF. Remova o GIF ou escolha outra rede.","posts.form.warnings.image_too_large":"A imagem passa do limite de :max (a sua tem :current).","posts.form.warnings.video_too_large":"O vídeo passa do limite de :max (o seu tem :current).","posts.form.warnings.video_too_long":"O vídeo dura :current, mas este tipo permite no máximo :max.","posts.form.warnings.aspect_ratio_too_narrow":"A proporção :current está muito alta (mínimo :min).","posts.form.warnings.aspect_ratio_too_wide":"A proporção :current está muito larga (máximo :max).","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.ai.generate.button_tooltip":"Gerar com IA","posts.ai.generate.title":"Gerar post com IA","posts.ai.generate.description":"Descreva sobre o que o post deve ser. A IA vai usar o contexto da sua marca pra escrever.","posts.ai.generate.prompt_label":"Sobre o que é esse post?","posts.ai.generate.prompt_placeholder":"ex: anunciar nossa nova feature de geração de imagens pra carrosséis","posts.ai.generate.preview_label":"Prévia","posts.ai.generate.start":"Gerar","posts.ai.generate.apply":"Usar este conteúdo","posts.ai.generate.retry":"Tentar de novo","posts.ai.generate.cancel":"Cancelar","posts.ai.review.button_tooltip":"Revisar com IA","posts.ai.review.title":"Revisar post com IA","posts.ai.review.description":"A IA analisa gramática, ortografia e clareza. Aplique as sugestões com as quais você concorda.","posts.ai.review.loading":"Revisando seu texto...","posts.ai.review.no_issues":"Nenhum problema encontrado. Tudo certo.","posts.ai.review.original":"Original","posts.ai.review.suggestion":"Sugestão","posts.ai.review.apply":"Aplicar","posts.ai.review.apply_all":"Aplicar todas","posts.ai.review.applied":"Aplicada","posts.ai.review.cancel":"Cancelar","posts.show.title":"Detalhes do Post","posts.show.edit":"Editar","posts.show.back":"Voltar","posts.show.no_content":"Sem legenda","posts.show.platforms":"Plataformas","posts.show.no_platforms":"Nenhuma plataforma selecionada.","posts.show.view_on_platform":"Ver na plataforma","posts.show.published_on":"Publicado em :date","posts.show.scheduled_for":"Agendado para :date","posts.show.draft":"Rascunho","posts.show.status_pending":"Pendente","posts.show.metrics":"Métricas","posts.show.metrics_loading":"Carregando métricas…","posts.show.metrics_unavailable":"Métricas ainda não disponíveis para esta plataforma.","posts.show.metrics_empty":"Nenhuma métrica retornada.","posts.edit.title":"Editar Post","posts.edit.view_title":"Visualizar Post","posts.edit.labels":"Etiqueta","posts.edit.signatures":"Assinaturas","posts.edit.schedule":"Agendar","posts.edit.delete":"Excluir","posts.edit.schedule_for":"Agendar para","posts.edit.unschedule":"Desagendar","posts.edit.saving":"Salvando...","posts.edit.saved":"Salvo","posts.edit.draft":"Rascunho","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.preview_empty.title":"Nenhuma plataforma selecionada","posts.edit.preview_empty.description":"Selecione uma plataforma para publicar e ver o preview.","posts.edit.drop_zone_title":"Adicionar mídia","posts.edit.drop_zone_subtitle":"Arraste arquivos ou clique para selecionar","posts.edit.add":"Adicionar","posts.edit.publish_to":"Publicar em","posts.edit.organize":"Organizar","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.compliance_incomplete":"Algumas configurações de plataforma estão incompletas ou incompatíveis com a mídia anexada.","posts.edit.compliance.requires_media":"Adicione uma imagem ou vídeo para publicar aqui.","posts.edit.compliance.too_many_files":"Apenas :max arquivo(s) permitido(s) para este formato.","posts.edit.compliance.too_few_files":"Adicione pelo menos :min arquivos para este formato.","posts.edit.compliance.no_videos":"Apenas imagens são permitidas neste formato.","posts.edit.compliance.no_images":"Apenas vídeos são permitidos neste formato.","posts.edit.compliance.no_gifs":"GIFs não são suportados aqui.","posts.edit.compliance.video_too_large":"Vídeo excede o limite de tamanho desta plataforma.","posts.edit.compliance.video_too_long":"Vídeo deve ter menos de :seconds segundos neste formato.","posts.edit.compliance.image_too_large":"Imagem excede o limite de tamanho desta plataforma.","posts.edit.compliance.aspect_ratio_invalid":"A proporção da imagem não é suportada por este formato.","posts.edit.compliance.no_content_type":"Escolha um tipo de conteúdo para esta plataforma.","posts.edit.publishing":"Publicando...","posts.edit.publishing_overlay_title":"Seu post está sendo publicado","posts.edit.publishing_overlay_subtitle":"Isso pode levar alguns instantes. Você pode sair desta página sem problemas.","posts.edit.scheduled_overlay_title":"Este post está agendado","posts.edit.scheduled_overlay_subtitle":"Agendado para :date. Desagende para fazer alterações.","posts.edit.unschedule_cta":"Desagendar para editar","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.media_picker.title":"Escolher da galeria","posts.edit.media_picker.search":"Buscar mídia...","posts.edit.media_picker.empty":"Sua galeria ainda está vazia","posts.edit.media_picker.cancel":"Cancelar","posts.edit.media_picker.add":"Adicionar","posts.edit.media_picker.add_count":"Adicionar :count","posts.edit.emoji_picker.search":"Buscar emoji","posts.edit.emoji_picker.empty":"Nenhum emoji encontrado","posts.edit.emoji_picker.recent":"Usados recentemente","posts.edit.emoji_picker.smileys":"Sorrisos e emoções","posts.edit.emoji_picker.people":"Pessoas e corpo","posts.edit.emoji_picker.nature":"Animais e natureza","posts.edit.emoji_picker.food":"Comidas e bebidas","posts.edit.emoji_picker.activities":"Atividades","posts.edit.emoji_picker.travel":"Viagens e lugares","posts.edit.emoji_picker.objects":"Objetos","posts.edit.emoji_picker.symbols":"Símbolos","posts.edit.emoji_picker.flags":"Bandeiras","posts.edit.status.scheduled":"Agendado","posts.edit.status.published":"Publicado","posts.edit.status.publishing":"Publicando...","posts.edit.status.failed":"Falhou","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.signatures_modal.search":"Buscar assinaturas...","posts.edit.signatures_modal.no_results":"Nenhuma assinatura 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.tiktok_photo.label":"Carrossel de fotos","posts.content_types.tiktok_photo.description":"Até 35 fotos em um carrossel deslizável","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 padrão","posts.content_types.pinterest_video_pin.label":"Pin de Vídeo","posts.content_types.pinterest_video_pin.description":"Pin de vídeo (4s - 15min)","posts.content_types.pinterest_carousel.label":"Carrossel","posts.content_types.pinterest_carousel.description":"Carrossel multi-imagem (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.deleted":"Post excluído com sucesso!","posts.flash.duplicated":"Post duplicado como rascunho.","posts.flash.cannot_edit_published":"Posts publicados não podem ser editados.","posts.flash.cannot_delete_published":"Posts publicados não podem ser excluídos.","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","posts.errors.account_token_expired":"Sessão da conta social expirou — reconecte a conta","posts.delete.title":"Excluir post?","posts.delete.description":"Esta ação não pode ser desfeita. O post e todas as suas mídias serão removidos permanentemente.","posts.delete.confirm":"Sim, excluir","posts.delete.cancel":"Cancelar","posts.create.title":"Criar novo post","posts.create.description":"Escolha como quer começar.","posts.create.scratch_title":"Começar do zero","posts.create.scratch_description":"Abre um post em branco pra você escrever tudo.","posts.create.ai_title":"Gerar com IA","posts.create.ai_description":"Descreva o que quer e a IA gera o conteúdo pra você.","posts.create.ai_configure_description":"Escolha o formato e descreva o post que quer criar.","posts.create.template_title":"Usar um template","posts.create.template_description":"Escolha um dos nossos templates e personalize.","posts.create.coming_soon":"Em breve","posts.create.preview.image_title":"Título da imagem","posts.create.preview.image_body":"Texto da imagem","posts.create.steps.format_title":"Escolha um formato","posts.create.steps.format_description":"Selecione o tipo de post que deseja criar.","posts.create.steps.account_title":"Escolha uma conta","posts.create.steps.account_description":"Selecione a conta social para publicar.","posts.create.steps.media_title":"Opções de mídia","posts.create.steps.media_carousel":"Quantos slides?","posts.create.steps.media_optional":"Incluir imagens?","posts.create.steps.media_optional_label":"Quantas imagens?","posts.create.steps.media_none":"Nenhuma","posts.create.steps.media_count_label":"Número de imagens","posts.create.steps.prompt_title":"Descreva seu post","posts.create.steps.prompt_label":"Sobre o que é este post?","posts.create.steps.prompt_placeholder":"Ex. Anunciar nossa nova função de carrossel para o Instagram","posts.create.steps.preview_error":"Algo deu errado. Por favor, tente novamente.","posts.create.steps.loading_page_title":"Gerando seu post","posts.create.steps.loading_eta":"Tempo estimado: cerca de :minutes.","posts.create.steps.loading_eta_minute_one":"1 minuto","posts.create.steps.loading_eta_minute_other":":count minutos","posts.create.steps.loading_leave_title":"Você pode continuar trabalhando.","posts.create.steps.loading_leave_body":"A gente te avisa assim que o post ficar pronto.","posts.create.steps.loading_leave_cta":"Ir pro calendário","posts.create.steps.loading_create_another_cta":"Criar outro post","posts.create.steps.loading_tip_credits":"Cada imagem AI consome cerca de 15 créditos.","posts.create.steps.loading_tip_edit":"Você poderá editar tudo quando o post ficar pronto.","posts.create.steps.loading_tip_draft":"Posts gerados vão direto pros seus rascunhos.","posts.create.steps.loading_tip_brand":"Ajuste sua marca pra influenciar os próximos posts.","posts.create.steps.loading_tip_carousel":"Carrosséis geram um slide por imagem solicitada.","posts.create.steps.loading_tip_quality":"A qualidade equilibra velocidade e custo.","posts.create.steps.create":"Criar post","posts.create.steps.back":"Voltar","posts.create.steps.next":"Continuar","posts.create.steps.cancel":"Cancelar","posts.create.steps.discard":"Descartar","posts.create.steps.retry":"Tentar novamente","posts.create.steps.no_platforms":"Nenhuma conta conectada","posts.create.steps.connect_first":"Conecte pelo menos uma conta social para usar a geração com IA.","posts.create.steps.format.instagram_feed":"Post no Feed do Instagram","posts.create.steps.format.instagram_carousel":"Carrossel do Instagram","posts.create.steps.format.linkedin_post":"Post no LinkedIn","posts.create.steps.format.linkedin_page_post":"Post em Página do LinkedIn","posts.create.steps.format.x_post":"Post no X","posts.create.steps.format.bluesky_post":"Post no Bluesky","posts.create.steps.format.threads_post":"Post no Threads","posts.create.steps.format.mastodon_post":"Post no Mastodon","posts.create.steps.format.facebook_post":"Post no Facebook","posts.create.steps.format.pinterest_pin":"Pin no Pinterest","posts.create.steps.format.instagram_story":"Story do Instagram","posts.create.steps.format.facebook_story":"Story do Facebook","posts.templates.browser_title":"Escolha um template","posts.templates.browser_description":"Comece com um template pronto e adapte ao seu jeito.","posts.templates.all_platforms":"Todas as plataformas","posts.templates.platform_search_placeholder":"Buscar plataforma…","posts.templates.no_platform_match":"Nenhuma plataforma corresponde.","posts.templates.use_this":"Usar este template","posts.templates.no_templates":"Nenhum template disponível.","posts.templates.applying":"Aplicando template…","posts.templates.search_placeholder":"Buscar templates…","posts.templates.no_search_results":"Nenhum template encontrado","posts.templates.try_different_search":"Tente outra palavra-chave ou limpe a busca.","posts.templates.slides_count":"{count} slide|{count} slides","posts.templates.category.product_launch":"Lançamento de produto","posts.templates.category.promotion":"Promoção","posts.templates.category.educational":"Educacional","posts.templates.category.behind_the_scenes":"Bastidores","posts.templates.category.testimonial":"Depoimento","posts.templates.category.industry_tip":"Dica do setor","posts.templates.category.event":"Evento","posts.templates.category.engagement":"Engajamento","settings.title":"Configurações","settings.description":"Gerencie seu perfil e configurações da conta","settings.hub.title":"Configurações","settings.hub.description":"Escolha o que você quer gerenciar.","settings.hub.profile.title":"Perfil","settings.hub.profile.description":"Atualize suas informações pessoais, senha e preferências de notificações.","settings.hub.workspace.title":"Workspace","settings.hub.workspace.description":"Configure seu workspace, marca, membros e chaves de API.","settings.hub.account.title":"Conta","settings.hub.account.description":"Gerencie informações da conta, uso e faturamento.","settings.nav.profile":"Perfil","settings.nav.authentication":"Autenticação","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.authentication.title":"Autenticação","settings.authentication.page_title":"Configurações de autenticação","settings.authentication.sessions.title":"Sessões ativas","settings.authentication.sessions.description":"Se você notar algo suspeito, encerre as sessões em outros dispositivos.","settings.authentication.sessions.unknown_browser":"Navegador desconhecido","settings.authentication.sessions.unknown_ip":"IP desconhecido","settings.authentication.sessions.on":"em","settings.authentication.sessions.active_now":"Ativa agora","settings.authentication.sessions.log_out_others":"Encerrar outras sessões","settings.authentication.sessions.modal_title":"Encerrar outras sessões","settings.authentication.sessions.modal_description_password":"Digite sua senha atual para confirmar o encerramento das outras sessões.","settings.authentication.sessions.modal_description_email":"Digite seu e-mail para confirmar o encerramento das outras sessões.","settings.authentication.sessions.password_placeholder":"Senha atual","settings.authentication.sessions.email_placeholder":"Seu e-mail","settings.authentication.sessions.cancel":"Cancelar","settings.authentication.sessions.submit":"Encerrar outras sessões","settings.authentication.sessions.email_mismatch":"O e-mail não corresponde à sua conta.","settings.authentication.sessions.flash_logged_out":"Outras sessões foram encerradas.","settings.authentication.password.update_title":"Atualizar senha","settings.authentication.password.set_title":"Definir uma senha","settings.authentication.password.update_description":"Use uma senha longa e aleatória para manter sua conta segura.","settings.authentication.password.set_description":"Adicione uma senha para entrar sem precisar de um provedor conectado.","settings.authentication.password.current_password":"Senha atual","settings.authentication.password.new_password":"Nova senha","settings.authentication.password.confirm_password":"Confirmar senha","settings.authentication.password.save":"Salvar senha","settings.authentication.password.set":"Definir senha","settings.authentication.providers.title":"Contas conectadas","settings.authentication.providers.description":"Faça login mais rápido usando esses provedores conectados.","settings.authentication.providers.connected":"Conectada","settings.authentication.providers.not_connected":"Não conectada","settings.authentication.providers.connect":"Conectar","settings.authentication.providers.disconnect":"Desconectar","settings.authentication.providers.flash_disconnected":":provider desconectada com sucesso.","settings.authentication.providers.flash_connected":":provider conectada com sucesso.","settings.authentication.providers.flash_already_linked":"Essa conta do :provider já está vinculada a outro usuário.","settings.authentication.providers.flash_cannot_disconnect":"Você não pode desconectar seu único método de login. Defina uma senha ou conecte outro provedor primeiro.","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_password":"Uma vez excluída, todos os seus recursos e dados também serão permanentemente removidos. Digite sua senha para confirmar.","settings.delete_account.modal_description_email":"Uma vez excluída, todos os seus recursos e dados também serão permanentemente removidos. Digite o seu e-mail :email para confirmar.","settings.delete_account.password":"Senha","settings.delete_account.password_placeholder":"Senha","settings.delete_account.email_placeholder":"Seu e-mail","settings.delete_account.email_mismatch":"O e-mail não corresponde à sua conta.","settings.delete_account.cancel":"Cancelar","settings.delete_account.confirm":"Excluir conta","settings.workspace.tabs.workspace":"Workspace","settings.workspace.tabs.brand":"Marca","settings.workspace.tabs.users":"Membros","settings.workspace.tabs.api_keys":"API Keys","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.name":"Nome do workspace","settings.brand.name_placeholder":"Minha marca","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.brand_color":"Cor da marca","settings.brand.background_color":"Cor de fundo","settings.brand.text_color":"Cor do texto","settings.brand.font":"Fonte","settings.brand.image_style":"Estilo das imagens","settings.brand.image_style_description":"Estilo visual aplicado ao gerar imagens de slides e capas para posts com AI.","settings.brand.image_style_cinematic":"Cinematográfico","settings.brand.image_style_illustration":"Ilustração","settings.brand.image_style_isometric_3d":"Isométrico","settings.brand.image_style_cartoon":"Cartoon","settings.brand.image_style_typographic":"Tipográfico","settings.brand.image_style_infographic":"Infográfico","settings.brand.image_style_minimalist":"Minimalista","settings.brand.image_style_mockup":"Mockup","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.roles.viewer":"Visualizador","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.account.tabs.account":"Conta","settings.account.tabs.usage":"Uso","settings.account.tabs.billing":"Faturamento","settings.account.title":"Configurações da conta","settings.account.description":"Gerencie o nome da conta e o e-mail de cobrança","settings.account.name":"Nome da conta","settings.account.name_placeholder":"Minha Empresa","settings.account.billing_email":"E-mail de cobrança","settings.account.billing_email_placeholder":"cobranca@empresa.com","settings.account.billing_email_hint":"Este e-mail será usado para faturas e comunicações de cobrança do Stripe.","settings.account.submit":"Salvar","settings.flash.account_updated":"Conta atualizada com sucesso!","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.signatures":"Assinaturas","sidebar.workspace.labels":"Etiquetas","sidebar.workspace.assets":"Mídias","sidebar.workspace.api_keys":"API Keys","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.support":"Suporte","sidebar.analytics":"Analytics","sidebar.settings":"Configurações","sidebar.posts.calendar":"Calendário","sidebar.posts.all":"Todos","sidebar.posts.scheduled":"Agendados","sidebar.posts.posted":"Publicados","sidebar.posts.drafts":"Rascunhos","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","signatures.title":"Assinaturas","signatures.description":"Crie assinaturas reutilizáveis pra anexar rapidamente nos seus posts","signatures.search":"Buscar assinaturas...","signatures.new":"Nova assinatura","signatures.empty_title":"Nenhuma assinatura ainda","signatures.empty_description":"Crie assinaturas pra anexar hashtags, links ou qualquer texto reutilizável nos seus posts","signatures.no_search_results":"Nenhuma assinatura corresponde à busca","signatures.try_different_search":"Tente outra palavra-chave ou limpe a busca.","signatures.table.name":"Nome","signatures.table.content":"Conteúdo","signatures.table.created_at":"Criado em","signatures.actions.edit":"Editar assinatura","signatures.actions.delete":"Excluir assinatura","signatures.create.title":"Criar assinatura","signatures.create.description":"Dê um nome à sua assinatura e o conteúdo pra anexar (hashtags, links, texto livre — o que você reutiliza).","signatures.create.name":"Nome","signatures.create.name_placeholder":"ex: Marketing, Viagem, Encerramento da marca","signatures.create.content":"Conteúdo","signatures.create.content_placeholder":"#marketing #socialmedia\nSaiba mais: https://suamarca.com","signatures.create.content_hint":"Hashtags, links, intros, assinaturas — qualquer coisa que você anexa nos posts.","signatures.create.submit":"Criar assinatura","signatures.create.submitting":"Criando...","signatures.edit.title":"Editar assinatura","signatures.edit.description":"Atualize o nome e o conteúdo desta assinatura.","signatures.edit.name":"Nome","signatures.edit.name_placeholder":"ex: Marketing, Viagem, Encerramento da marca","signatures.edit.content":"Conteúdo","signatures.edit.content_placeholder":"#marketing #socialmedia\nSaiba mais: https://suamarca.com","signatures.edit.content_hint":"Hashtags, links, intros, assinaturas — qualquer coisa que você anexa nos posts.","signatures.edit.submit":"Salvar alterações","signatures.edit.submitting":"Salvando...","signatures.delete.title":"Deletar assinatura","signatures.delete.description":"Tem certeza que quer deletar esta assinatura? Esta ação não pode ser desfeita.","signatures.delete.confirm":"Deletar","signatures.delete.cancel":"Cancelar","signatures.flash.created":"Assinatura criada.","signatures.flash.updated":"Assinatura atualizada.","signatures.flash.deleted":"Assinatura deletada.","usage.title":"Uso","usage.section_account":"Conta","usage.section_account_description":"Cotas e limites do seu plano :plan.","usage.section_ai":"Créditos AI","usage.section_ai_description":"Os créditos são debitados conforme você usa os recursos de AI. Eles são renovados no dia 1 de cada mês.","usage.workspaces":"Workspaces","usage.social_accounts":"Contas Sociais","usage.members":"Membros","usage.credits":"Créditos","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 um pouco sobre você ou seu projeto. Vamos usar pra personalizar os 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.brand_color":"Cor da marca","workspaces.create.background_color":"Cor de fundo","workspaces.create.text_color":"Cor do texto","workspaces.create.submit":"Criar workspace","workspaces.create.success":"Workspace criado. Conecte uma conta social para começar a postar.","workspaces.limit_reached":"Você atingiu o limite de workspaces do seu plano.","workspaces.flash.deleted":"Workspace excluído com sucesso."} \ No newline at end of file +{"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.connect_cta":"Conectar","accounts.no_accounts":"Nenhuma conta conectada ainda","accounts.no_accounts_description":"Conecte suas redes sociais para começar a agendar e publicar posts","accounts.no_search_results":"Nenhuma conta corresponde à sua busca","accounts.try_different_search":"Tente outra palavra-chave ou limpe a busca.","accounts.search":"Buscar contas...","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.table.account":"Conta","accounts.table.platform":"Plataforma","accounts.table.status":"Status","accounts.table.last_used":"Último uso","accounts.table.added":"Adicionada","accounts.table.active":"Ativa","accounts.never_used":"Nunca usada","accounts.status.connected":"Conectada","accounts.status.disconnected":"Desconectada","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.","accounts.popup_callback.title_success":"Conectado","accounts.popup_callback.title_error":"Erro","accounts.popup_callback.closing":"Esta janela será fechada automaticamente...","accounts.popup_callback.close_now":"Você pode fechar esta janela agora.","accounts.popup_callback.connected":"Conta conectada!","accounts.popup_callback.reconnected":"Conta reconectada!","accounts.popup_callback.error_connecting":"Erro ao conectar conta. Por favor, tente novamente.","accounts.popup_callback.error_connecting_page":"Erro ao conectar página. Por favor, tente novamente.","accounts.popup_callback.error_connecting_channel":"Erro ao conectar canal. Por favor, tente novamente.","accounts.popup_callback.session_expired":"Sessão expirada. Por favor, tente novamente.","accounts.popup_callback.workspace_not_found":"Workspace não encontrado.","accounts.popup_callback.invalid_state":"Estado inválido. Por favor, tente novamente.","accounts.popup_callback.failed_to_authenticate":"Falha na autenticação.","accounts.popup_callback.failed_to_get_profile":"Falha ao obter perfil.","accounts.popup_callback.page_not_found":"Página não encontrada.","accounts.popup_callback.channel_not_found":"Canal não encontrado.","accounts.popup_callback.no_facebook_pages":"Nenhuma página do Facebook encontrada. Você precisa ser administrador de pelo menos uma página.","accounts.popup_callback.no_facebook_instagram_pages":"Nenhuma página do Facebook com conta do Instagram vinculada foi encontrada.","accounts.popup_callback.no_youtube_channels":"Nenhum canal do YouTube encontrado. Por favor, crie um canal primeiro.","accounts.popup_callback.not_linkedin_admin":"Você não é administrador de nenhuma página do LinkedIn.","analytics.no_accounts":"Nenhuma conta conectada com analytics.","analytics.no_accounts_match":"Nenhuma conta corresponde.","analytics.search_account":"Buscar conta…","analytics.select_account":"Selecione uma conta para ver analytics.","analytics.no_data":"Nenhum dado de analytics disponível.","analytics.metrics.avg_view_duration":"Duração Média (s)","analytics.metrics.avg_view_percentage":"Visualização Média","analytics.metrics.bookmarks":"Salvos","analytics.metrics.clicks":"Cliques","analytics.metrics.comments":"Comentários","analytics.metrics.engagement":"Engajamento","analytics.metrics.favourites":"Favoritos","analytics.metrics.followers":"Seguidores","analytics.metrics.following":"Seguindo","analytics.metrics.impressions":"Impressões","analytics.metrics.interactions":"Interações","analytics.metrics.likes":"Curtidas","analytics.metrics.minutes_watched":"Minutos Assistidos","analytics.metrics.organic_followers":"Seguidores Orgânicos","analytics.metrics.outbound_clicks":"Cliques Externos","analytics.metrics.page_followers":"Seguidores da Página","analytics.metrics.page_reach":"Alcance da Página","analytics.metrics.page_views":"Visualizações da Página","analytics.metrics.paid_followers":"Seguidores Pagos","analytics.metrics.pin_click_rate":"Taxa de Clique em Pins","analytics.metrics.pin_clicks":"Cliques em Pins","analytics.metrics.posts_engagement":"Engajamento dos Posts","analytics.metrics.posts_reach":"Alcance dos Posts","analytics.metrics.quotes":"Citações","analytics.metrics.reach":"Alcance","analytics.metrics.reblogs":"Reblogs","analytics.metrics.recent_comments":"Comentários Recentes","analytics.metrics.recent_likes":"Curtidas Recentes","analytics.metrics.recent_shares":"Compartilhamentos Recentes","analytics.metrics.replies":"Respostas","analytics.metrics.reposts":"Reposts","analytics.metrics.retweets":"Retweets","analytics.metrics.saves":"Salvos","analytics.metrics.shares":"Compartilhamentos","analytics.metrics.subscribers_gained":"Inscritos Ganhos","analytics.metrics.subscribers_lost":"Inscritos Perdidos","analytics.metrics.total_likes":"Curtidas Totais","analytics.metrics.video_views":"Visualizações de Vídeo","analytics.metrics.videos":"Vídeos","analytics.metrics.views":"Visualizações","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.add_to_post":"Adicionar ao post","assets.search_placeholder":"Buscar mídia...","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","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.signatures.title":"Assinaturas","auth.slides.signatures.description":"Salve assinaturas reutilizáveis (hashtags, links, encerramentos) e anexe nos posts com um clique.","auth.or_continue_with":"Ou continue com","auth.google_login":"Entrar com Google","auth.google_signup":"Cadastrar com Google","auth.github_login":"Entrar com GitHub","auth.github_signup":"Cadastrar com GitHub","auth.github_email_unavailable":"Não foi possível obter seu e-mail do GitHub. Torne seu e-mail público ou conceda a permissão de e-mail e tente novamente.","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":"Faturamento","billing.upgrade_dialog.title":"Faça upgrade do seu plano","billing.upgrade_dialog.description":"Escolha um plano que se encaixe nas suas necessidades.","billing.upgrade_dialog.current_plan":"Plano atual","billing.upgrade_dialog.current_short":"Atual","billing.upgrade_dialog.current_badge":"Atual","billing.upgrade_dialog.subscribe":"Assinar","billing.upgrade_dialog.switch":"Mudar para este plano","billing.upgrade_dialog.switch_short":"Mudar","billing.upgrade_dialog.switch_to_yearly":"Mudar para anual","billing.upgrade_dialog.switch_to_monthly":"Mudar para mensal","billing.upgrade_dialog.unavailable":"Indisponível","billing.upgrade_dialog.reasons.workspace_limit":"Você atingiu o limite de workspaces do seu plano. Faça upgrade pra criar mais.","billing.upgrade_dialog.reasons.social_account_limit":"Você atingiu o limite de contas sociais do seu plano. Faça upgrade pra conectar mais.","billing.upgrade_dialog.reasons.member_limit":"Você atingiu o limite de membros do seu plano. Faça upgrade pra convidar mais pessoas.","billing.subscribe.page_title":"Escolha seu plano","billing.subscribe.eyebrow":"Preços","billing.subscribe.title":"Escolha o plano ideal pra você","billing.subscribe.description":"Escolha o plano que combina com você. Cobrança mensal ou anual.","billing.subscribe.monthly":"Mensal","billing.subscribe.yearly":"Anual","billing.subscribe.per_month":"mensal","billing.subscribe.per_year":"anual","billing.subscribe.billed_monthly":"Cobrança mensal","billing.subscribe.billed_yearly":"Cobrança anual","billing.subscribe.features_included":"O que está incluído:","billing.subscribe.everything_in":"Tudo do :plan, mais:","billing.subscribe.save_months":"2 meses grátis","billing.subscribe.popular":"Mais popular","billing.subscribe.subscribe_cta":"Assinar","billing.subscribe.prices.starter.monthly":"R$ 95","billing.subscribe.prices.starter.yearly_per_month":"R$ 79","billing.subscribe.prices.starter.yearly":"R$ 950","billing.subscribe.prices.plus.monthly":"R$ 145","billing.subscribe.prices.plus.yearly_per_month":"R$ 121","billing.subscribe.prices.plus.yearly":"R$ 1450","billing.subscribe.prices.pro.monthly":"R$ 245","billing.subscribe.prices.pro.yearly_per_month":"R$ 204","billing.subscribe.prices.pro.yearly":"R$ 2450","billing.subscribe.prices.max.monthly":"R$ 495","billing.subscribe.prices.max.yearly_per_month":"R$ 413","billing.subscribe.prices.max.yearly":"R$ 4950","billing.subscribe.features.social_accounts":":count contas sociais","billing.subscribe.features.workspaces":":count workspaces","billing.subscribe.features.members":":count membros da equipe","billing.subscribe.features.credits":":count créditos IA/mês","billing.subscribe.credit_tooltips.starter":"Em média 150 posts de tamanho médio + 5 imagens de IA por mês.","billing.subscribe.credit_tooltips.plus":"Em média 300 posts de tamanho médio + 10 imagens de IA por mês.","billing.subscribe.credit_tooltips.pro":"Em média 700 posts de tamanho médio + 30 imagens de IA por mês.","billing.subscribe.credit_tooltips.max":"Em média 2.000 posts de tamanho médio + 100 imagens de IA por mês.","billing.plan.title":"Plano","billing.plan.description":"Gerencie seu plano de assinatura.","billing.plan.change":"Mudar plano","billing.plan.label":"Plano","billing.plan.price":"Preço","billing.plan.month":"mês","billing.plan.trial":"Trial","billing.plan.active":"Ativo","billing.plan.past_due":"Vencido","billing.plan.cancelling":"Cancelando","billing.plan.trial_ends":"Teste termina em","billing.subscription.title":"Assinatura","billing.subscription.description":"Gerencie seu método de pagamento, dados de cobrança e assinatura.","billing.subscription.payment_method":"Método de pagamento","billing.subscription.no_payment_method":"Nenhum método de pagamento cadastrado.","billing.subscription.expires_on":"Expira em :month/:year","billing.subscription.manage_label":"Assinatura","billing.subscription.manage_stripe":"Gerenciar no Stripe","billing.invoices.title":"Faturas","billing.invoices.description":"Baixe suas faturas anteriores.","billing.invoices.empty":"Nenhuma fatura encontrada","billing.invoices.paid":"Pago","billing.flash.plan_changed":"Você está agora no plano :plan.","billing.flash.cannot_manage":"Apenas o owner da conta pode gerenciar a cobrança.","billing.flash.cannot_downgrade.workspaces":"Não é possível mudar para :plan: você tem :count workspaces mas o plano só permite :limit.","billing.flash.cannot_downgrade.social_accounts":"Não é possível mudar para :plan: você tem :count contas sociais mas o plano só permite :limit.","billing.flash.cannot_downgrade.members":"Não é possível mudar para :plan: você tem :count membros (incluindo convites) mas o plano só permite :limit.","billing.flash.credits_exhausted":"Sem créditos de IA — você usou seus :limit créditos mensais. Faça upgrade do plano ou aguarde até o próximo mês.","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","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","comments.today":"Hoje","comments.yesterday":"Ontem","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.confirm_modal.delete_keyword":"deletar","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.date_range_picker.placeholder":"Escolha um período","common.date_range_picker.today":"Hoje","common.date_range_picker.yesterday":"Ontem","common.date_range_picker.last_7_days":"Últimos 7 dias","common.date_range_picker.last_30_days":"Últimos 30 dias","common.date_range_picker.last_3_months":"Últimos 3 meses","common.date_range_picker.last_6_months":"Últimos 6 meses","common.date_range_picker.last_12_months":"Últimos 12 meses","common.date_range_picker.this_month":"Este mês","common.date_range_picker.last_month":"Mês passado","common.date_range_picker.year_to_date":"Desde o início do ano","common.date_range_picker.last_year":"Ano passado","common.cancel":"Cancelar","common.clear":"Limpar","common.close":"Fechar","common.loading_more":"Carregando mais...","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.no_search_results":"Nenhuma etiqueta corresponde à sua busca","labels.try_different_search":"Tente outra palavra-chave ou limpe a busca.","labels.create_first_label":"Crie sua primeira etiqueta","labels.table.name":"Nome","labels.table.created_at":"Criado","labels.actions.edit":"Editar etiqueta","labels.actions.delete":"Excluir 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.mentioned.subject":":name mencionou você no TryPost","mail.mentioned.title":":name mencionou você","mail.mentioned.intro":":name mencionou você num comentário.","mail.mentioned.cta":"Ver comentário","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","notifications.post_ready.title":"Seu post está pronto","notifications.post_ready.body":"A AI terminou. Toque pra revisar e publicar.","notifications.account_disconnected.title":"Conta do :platform desconectada","notifications.account_disconnected.body":":account precisa ser reconectada","notifications.account_token_expired.title":"Conta do :platform precisa ser reconectada","notifications.account_token_expired.body":"Sessão de :account expirou — reconecte pra continuar postando","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.no_search_results":"Nenhum post corresponde à sua busca","posts.try_different_search":"Tente outra palavra-chave ou limpe a busca.","posts.start_creating":"Comece criando seu primeiro post.","posts.filter_by_label":"Filtrar por label","posts.label_search_placeholder":"Buscar labels...","posts.no_labels":"Nenhuma label encontrada.","posts.clear_label_filter":"Limpar filtro de labels","posts.table.post":"Post","posts.table.status":"Status","posts.table.content":"Conteúdo","posts.table.platforms":"Plataformas","posts.table.labels":"Etiquetas","posts.table.scheduled_at":"Data","posts.table.actions":"","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","posts.actions.duplicate":"Duplicar","posts.actions.copy_id":"Copiar ID","posts.actions.copied":"ID copiado para a área de transferência","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.content_exceeds_platform":":platform: longo demais por :over caracteres (máx :limit).","posts.form.tiktok.settings":"Configurações do TikTok","posts.form.tiktok.variant_label":"Tipo de publicação","posts.form.tiktok.variant.video":"Vídeo","posts.form.tiktok.variant.photo":"Carrossel de fotos","posts.form.tiktok.posting_to":"Publicando em","posts.form.tiktok.privacy_level":"Quem pode ver este vídeo?","posts.form.tiktok.privacy_placeholder":"Selecione a visibilidade","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.private_disabled_branded":"Conteúdo de marca não pode ser privado.","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.disclose":"Divulgar conteúdo do vídeo","posts.form.tiktok.disclose_hint":"Ative para divulgar que este vídeo promove bens ou serviços em troca de algo de valor. Seu vídeo pode promover você, terceiros ou ambos.","posts.form.tiktok.promotional_organic_title":"Sua foto/vídeo será rotulado como \"Conteúdo Promocional\".","posts.form.tiktok.promotional_paid_title":"Sua foto/vídeo será rotulado como \"Parceria paga\".","posts.form.tiktok.promotional_description":"Isso não poderá ser alterado após a publicação.","posts.form.tiktok.compliance_incomplete":"Você precisa indicar se o conteúdo promove você mesmo, terceiros ou ambos.","posts.form.tiktok.privacy_required":"A visibilidade do TikTok é obrigatória ao publicar.","posts.form.tiktok.branded_cleared_private":"A visibilidade foi limpa porque conteúdo de marca não pode ser privado.","posts.form.tiktok.interaction_disabled_by_creator":"Desativado pelas configurações da sua conta TikTok.","posts.form.tiktok.max_duration_exceeded":"Vídeo tem :duration s mas esta conta só permite vídeos de até :max s.","posts.form.tiktok.processing_hint":"Após publicar, pode levar alguns minutos para o conteúdo ser processado e aparecer no seu perfil TikTok.","posts.form.tiktok.brand_organic":"Sua marca","posts.form.tiktok.brand_organic_hint":"Você está promovendo você mesmo ou sua própria marca. Este vídeo será classificado como Brand Organic.","posts.form.tiktok.brand_content":"Conteúdo patrocinado","posts.form.tiktok.brand_content_hint":"Você está promovendo outra marca ou terceiros. Este vídeo será classificado como Branded Content.","posts.form.tiktok.compliance.agree":"Ao publicar, você concorda com a","posts.form.tiktok.compliance.music_usage":"Confirmação de Uso de Música","posts.form.tiktok.compliance.and":"e","posts.form.tiktok.compliance.branded_policy":"Política de Conteúdo Patrocinado","posts.form.instagram.settings":"Configurações do Instagram","posts.form.instagram.posting_to":"Publicando em","posts.form.instagram.variant_label":"Tipo de publicação","posts.form.instagram.variant.feed":"Post","posts.form.instagram.variant.reel":"Reel","posts.form.instagram.variant.story":"Story","posts.form.instagram.aspect_label":"Proporção","posts.form.instagram.aspect.square":"Quadrado (1:1)","posts.form.instagram.aspect.portrait":"Retrato (4:5)","posts.form.instagram.aspect.landscape":"Paisagem (16:9)","posts.form.instagram.aspect.original":"Original","posts.form.facebook.settings":"Configurações do Facebook","posts.form.facebook.posting_to":"Publicando em","posts.form.facebook.variant_label":"Tipo de publicação","posts.form.facebook.variant.post":"Post","posts.form.facebook.variant.reel":"Reel","posts.form.facebook.variant.story":"Story","posts.form.linkedin.settings":"Configurações do LinkedIn","posts.form.linkedin.settings_page":"Configurações da Página do LinkedIn","posts.form.linkedin.posting_to":"Publicando em","posts.form.linkedin.variant_label":"Tipo de publicação","posts.form.linkedin.variant.post":"Post","posts.form.linkedin.variant.carousel":"Carrossel","posts.form.pinterest.settings":"Configurações do Pinterest","posts.form.pinterest.posting_to":"Publicando em","posts.form.pinterest.variant_label":"Tipo de pin","posts.form.pinterest.variant.pin":"Pin","posts.form.pinterest.variant.video_pin":"Video Pin","posts.form.pinterest.variant.carousel":"Carrossel","posts.form.pinterest.board":"Quadro","posts.form.pinterest.select_board":"Selecione um quadro","posts.form.pinterest.no_boards":"Nenhum quadro do Pinterest encontrado. Crie um na sua conta do Pinterest primeiro.","posts.form.pinterest.search_board":"Pesquisar quadros...","posts.form.pinterest.no_board_found":"Nenhum quadro encontrado.","posts.form.pinterest.board_required":"Selecione um quadro do Pinterest para publicar este post.","posts.form.warnings.no_variant":"Escolha um tipo de publicação para continuar.","posts.form.warnings.requires_media":"Este tipo exige pelo menos uma imagem ou vídeo.","posts.form.warnings.max_files_exceeded":"Este tipo aceita até :max arquivos (você tem :current).","posts.form.warnings.min_files_required":"Este tipo exige pelo menos :min arquivos (você tem :current).","posts.form.warnings.no_video_allowed":"Este tipo não aceita vídeos.","posts.form.warnings.no_image_allowed":"Este tipo aceita apenas vídeos.","posts.form.warnings.gif_not_allowed":"Esta rede não aceita GIF. Remova o GIF ou escolha outra rede.","posts.form.warnings.image_too_large":"A imagem passa do limite de :max (a sua tem :current).","posts.form.warnings.video_too_large":"O vídeo passa do limite de :max (o seu tem :current).","posts.form.warnings.video_too_long":"O vídeo dura :current, mas este tipo permite no máximo :max.","posts.form.warnings.aspect_ratio_too_narrow":"A proporção :current está muito alta (mínimo :min).","posts.form.warnings.aspect_ratio_too_wide":"A proporção :current está muito larga (máximo :max).","posts.status.pending":"Pendente","posts.status.draft":"Rascunho","posts.status.scheduled":"Agendado","posts.status.publishing":"Publicando","posts.status.retrying":"Tentando novamente","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.ai.generate.button_tooltip":"Gerar com IA","posts.ai.generate.title":"Gerar post com IA","posts.ai.generate.description":"Descreva sobre o que o post deve ser. A IA vai usar o contexto da sua marca pra escrever.","posts.ai.generate.prompt_label":"Sobre o que é esse post?","posts.ai.generate.prompt_placeholder":"ex: anunciar nossa nova feature de geração de imagens pra carrosséis","posts.ai.generate.preview_label":"Prévia","posts.ai.generate.start":"Gerar","posts.ai.generate.apply":"Usar este conteúdo","posts.ai.generate.retry":"Tentar de novo","posts.ai.generate.cancel":"Cancelar","posts.ai.review.button_tooltip":"Revisar com IA","posts.ai.review.title":"Revisar post com IA","posts.ai.review.description":"A IA analisa gramática, ortografia e clareza. Aplique as sugestões com as quais você concorda.","posts.ai.review.loading":"Revisando seu texto...","posts.ai.review.no_issues":"Nenhum problema encontrado. Tudo certo.","posts.ai.review.original":"Original","posts.ai.review.suggestion":"Sugestão","posts.ai.review.apply":"Aplicar","posts.ai.review.apply_all":"Aplicar todas","posts.ai.review.applied":"Aplicada","posts.ai.review.cancel":"Cancelar","posts.show.title":"Detalhes do Post","posts.show.edit":"Editar","posts.show.back":"Voltar","posts.show.no_content":"Sem legenda","posts.show.platforms":"Plataformas","posts.show.no_platforms":"Nenhuma plataforma selecionada.","posts.show.view_on_platform":"Ver na plataforma","posts.show.published_on":"Publicado em :date","posts.show.scheduled_for":"Agendado para :date","posts.show.draft":"Rascunho","posts.show.status_pending":"Pendente","posts.show.metrics":"Métricas","posts.show.metrics_loading":"Carregando métricas…","posts.show.metrics_unavailable":"Métricas ainda não disponíveis para esta plataforma.","posts.show.metrics_empty":"Nenhuma métrica retornada.","posts.edit.title":"Editar Post","posts.edit.view_title":"Visualizar Post","posts.edit.labels":"Etiqueta","posts.edit.signatures":"Assinaturas","posts.edit.schedule":"Agendar","posts.edit.delete":"Excluir","posts.edit.schedule_for":"Agendar para","posts.edit.unschedule":"Desagendar","posts.edit.saving":"Salvando...","posts.edit.saved":"Salvo","posts.edit.draft":"Rascunho","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.preview_empty.title":"Nenhuma plataforma selecionada","posts.edit.preview_empty.description":"Selecione uma plataforma para publicar e ver o preview.","posts.edit.drop_zone_title":"Adicionar mídia","posts.edit.drop_zone_subtitle":"Arraste arquivos ou clique para selecionar","posts.edit.add":"Adicionar","posts.edit.publish_to":"Publicar em","posts.edit.organize":"Organizar","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.compliance_incomplete":"Algumas configurações de plataforma estão incompletas ou incompatíveis com a mídia anexada.","posts.edit.compliance.requires_media":"Adicione uma imagem ou vídeo para publicar aqui.","posts.edit.compliance.too_many_files":"Apenas :max arquivo(s) permitido(s) para este formato.","posts.edit.compliance.too_few_files":"Adicione pelo menos :min arquivos para este formato.","posts.edit.compliance.no_videos":"Apenas imagens são permitidas neste formato.","posts.edit.compliance.no_images":"Apenas vídeos são permitidos neste formato.","posts.edit.compliance.no_gifs":"GIFs não são suportados aqui.","posts.edit.compliance.video_too_large":"Vídeo excede o limite de tamanho desta plataforma.","posts.edit.compliance.video_too_long":"Vídeo deve ter menos de :seconds segundos neste formato.","posts.edit.compliance.image_too_large":"Imagem excede o limite de tamanho desta plataforma.","posts.edit.compliance.aspect_ratio_invalid":"A proporção da imagem não é suportada por este formato.","posts.edit.compliance.no_content_type":"Escolha um tipo de conteúdo para esta plataforma.","posts.edit.publishing":"Publicando...","posts.edit.publishing_overlay_title":"Seu post está sendo publicado","posts.edit.publishing_overlay_subtitle":"Isso pode levar alguns instantes. Você pode sair desta página sem problemas.","posts.edit.scheduled_overlay_title":"Este post está agendado","posts.edit.scheduled_overlay_subtitle":"Agendado para :date. Desagende para fazer alterações.","posts.edit.unschedule_cta":"Desagendar para editar","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.media_picker.title":"Escolher da galeria","posts.edit.media_picker.search":"Buscar mídia...","posts.edit.media_picker.empty":"Sua galeria ainda está vazia","posts.edit.media_picker.cancel":"Cancelar","posts.edit.media_picker.add":"Adicionar","posts.edit.media_picker.add_count":"Adicionar :count","posts.edit.emoji_picker.search":"Buscar emoji","posts.edit.emoji_picker.empty":"Nenhum emoji encontrado","posts.edit.emoji_picker.recent":"Usados recentemente","posts.edit.emoji_picker.smileys":"Sorrisos e emoções","posts.edit.emoji_picker.people":"Pessoas e corpo","posts.edit.emoji_picker.nature":"Animais e natureza","posts.edit.emoji_picker.food":"Comidas e bebidas","posts.edit.emoji_picker.activities":"Atividades","posts.edit.emoji_picker.travel":"Viagens e lugares","posts.edit.emoji_picker.objects":"Objetos","posts.edit.emoji_picker.symbols":"Símbolos","posts.edit.emoji_picker.flags":"Bandeiras","posts.edit.status.scheduled":"Agendado","posts.edit.status.published":"Publicado","posts.edit.status.publishing":"Publicando...","posts.edit.status.retrying":"Tentando novamente...","posts.edit.status.failed":"Falhou","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.signatures_modal.search":"Buscar assinaturas...","posts.edit.signatures_modal.no_results":"Nenhuma assinatura 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.tiktok_photo.label":"Carrossel de fotos","posts.content_types.tiktok_photo.description":"Até 35 fotos em um carrossel deslizável","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 padrão","posts.content_types.pinterest_video_pin.label":"Pin de Vídeo","posts.content_types.pinterest_video_pin.description":"Pin de vídeo (4s - 15min)","posts.content_types.pinterest_carousel.label":"Carrossel","posts.content_types.pinterest_carousel.description":"Carrossel multi-imagem (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.deleted":"Post excluído com sucesso!","posts.flash.duplicated":"Post duplicado como rascunho.","posts.flash.cannot_edit_published":"Posts publicados não podem ser editados.","posts.flash.cannot_delete_published":"Posts publicados não podem ser excluídos.","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","posts.errors.account_token_expired":"Sessão da conta social expirou — reconecte a conta","posts.delete.title":"Excluir post?","posts.delete.description":"Esta ação não pode ser desfeita. O post e todas as suas mídias serão removidos permanentemente.","posts.delete.confirm":"Sim, excluir","posts.delete.cancel":"Cancelar","posts.create.title":"Criar novo post","posts.create.description":"Escolha como quer começar.","posts.create.scratch_title":"Começar do zero","posts.create.scratch_description":"Abre um post em branco pra você escrever tudo.","posts.create.ai_title":"Gerar com IA","posts.create.ai_description":"Descreva o que quer e a IA gera o conteúdo pra você.","posts.create.ai_configure_description":"Escolha o formato e descreva o post que quer criar.","posts.create.template_title":"Usar um template","posts.create.template_description":"Escolha um dos nossos templates e personalize.","posts.create.coming_soon":"Em breve","posts.create.preview.image_title":"Título da imagem","posts.create.preview.image_body":"Texto da imagem","posts.create.steps.format_title":"Escolha um formato","posts.create.steps.format_description":"Selecione o tipo de post que deseja criar.","posts.create.steps.account_title":"Escolha uma conta","posts.create.steps.account_description":"Selecione a conta social para publicar.","posts.create.steps.media_title":"Opções de mídia","posts.create.steps.media_carousel":"Quantos slides?","posts.create.steps.media_optional":"Incluir imagens?","posts.create.steps.media_optional_label":"Quantas imagens?","posts.create.steps.media_none":"Nenhuma","posts.create.steps.media_count_label":"Número de imagens","posts.create.steps.prompt_title":"Descreva seu post","posts.create.steps.prompt_label":"Sobre o que é este post?","posts.create.steps.prompt_placeholder":"Ex. Anunciar nossa nova função de carrossel para o Instagram","posts.create.steps.preview_error":"Algo deu errado. Por favor, tente novamente.","posts.create.steps.loading_page_title":"Gerando seu post","posts.create.steps.loading_eta":"Tempo estimado: cerca de :minutes.","posts.create.steps.loading_eta_minute_one":"1 minuto","posts.create.steps.loading_eta_minute_other":":count minutos","posts.create.steps.loading_leave_title":"Você pode continuar trabalhando.","posts.create.steps.loading_leave_body":"A gente te avisa assim que o post ficar pronto.","posts.create.steps.loading_leave_cta":"Ir pro calendário","posts.create.steps.loading_create_another_cta":"Criar outro post","posts.create.steps.loading_tip_credits":"Cada imagem AI consome cerca de 15 créditos.","posts.create.steps.loading_tip_edit":"Você poderá editar tudo quando o post ficar pronto.","posts.create.steps.loading_tip_draft":"Posts gerados vão direto pros seus rascunhos.","posts.create.steps.loading_tip_brand":"Ajuste sua marca pra influenciar os próximos posts.","posts.create.steps.loading_tip_carousel":"Carrosséis geram um slide por imagem solicitada.","posts.create.steps.loading_tip_quality":"A qualidade equilibra velocidade e custo.","posts.create.steps.create":"Criar post","posts.create.steps.back":"Voltar","posts.create.steps.next":"Continuar","posts.create.steps.cancel":"Cancelar","posts.create.steps.discard":"Descartar","posts.create.steps.retry":"Tentar novamente","posts.create.steps.no_platforms":"Nenhuma conta conectada","posts.create.steps.connect_first":"Conecte pelo menos uma conta social para usar a geração com IA.","posts.create.steps.format.instagram_feed":"Post no Feed do Instagram","posts.create.steps.format.instagram_carousel":"Carrossel do Instagram","posts.create.steps.format.linkedin_post":"Post no LinkedIn","posts.create.steps.format.linkedin_page_post":"Post em Página do LinkedIn","posts.create.steps.format.x_post":"Post no X","posts.create.steps.format.bluesky_post":"Post no Bluesky","posts.create.steps.format.threads_post":"Post no Threads","posts.create.steps.format.mastodon_post":"Post no Mastodon","posts.create.steps.format.facebook_post":"Post no Facebook","posts.create.steps.format.pinterest_pin":"Pin no Pinterest","posts.create.steps.format.instagram_story":"Story do Instagram","posts.create.steps.format.facebook_story":"Story do Facebook","posts.templates.browser_title":"Escolha um template","posts.templates.browser_description":"Comece com um template pronto e adapte ao seu jeito.","posts.templates.all_platforms":"Todas as plataformas","posts.templates.platform_search_placeholder":"Buscar plataforma…","posts.templates.no_platform_match":"Nenhuma plataforma corresponde.","posts.templates.use_this":"Usar este template","posts.templates.no_templates":"Nenhum template disponível.","posts.templates.applying":"Aplicando template…","posts.templates.search_placeholder":"Buscar templates…","posts.templates.no_search_results":"Nenhum template encontrado","posts.templates.try_different_search":"Tente outra palavra-chave ou limpe a busca.","posts.templates.slides_count":"{count} slide|{count} slides","posts.templates.category.product_launch":"Lançamento de produto","posts.templates.category.promotion":"Promoção","posts.templates.category.educational":"Educacional","posts.templates.category.behind_the_scenes":"Bastidores","posts.templates.category.testimonial":"Depoimento","posts.templates.category.industry_tip":"Dica do setor","posts.templates.category.event":"Evento","posts.templates.category.engagement":"Engajamento","settings.title":"Configurações","settings.description":"Gerencie seu perfil e configurações da conta","settings.hub.title":"Configurações","settings.hub.description":"Escolha o que você quer gerenciar.","settings.hub.profile.title":"Perfil","settings.hub.profile.description":"Atualize suas informações pessoais, senha e preferências de notificações.","settings.hub.workspace.title":"Workspace","settings.hub.workspace.description":"Configure seu workspace, marca, membros e chaves de API.","settings.hub.account.title":"Conta","settings.hub.account.description":"Gerencie informações da conta, uso e faturamento.","settings.nav.profile":"Perfil","settings.nav.authentication":"Autenticação","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.authentication.title":"Autenticação","settings.authentication.page_title":"Configurações de autenticação","settings.authentication.sessions.title":"Sessões ativas","settings.authentication.sessions.description":"Se você notar algo suspeito, encerre as sessões em outros dispositivos.","settings.authentication.sessions.unknown_browser":"Navegador desconhecido","settings.authentication.sessions.unknown_ip":"IP desconhecido","settings.authentication.sessions.on":"em","settings.authentication.sessions.active_now":"Ativa agora","settings.authentication.sessions.log_out_others":"Encerrar outras sessões","settings.authentication.sessions.modal_title":"Encerrar outras sessões","settings.authentication.sessions.modal_description_password":"Digite sua senha atual para confirmar o encerramento das outras sessões.","settings.authentication.sessions.modal_description_email":"Digite seu e-mail para confirmar o encerramento das outras sessões.","settings.authentication.sessions.password_placeholder":"Senha atual","settings.authentication.sessions.email_placeholder":"Seu e-mail","settings.authentication.sessions.cancel":"Cancelar","settings.authentication.sessions.submit":"Encerrar outras sessões","settings.authentication.sessions.email_mismatch":"O e-mail não corresponde à sua conta.","settings.authentication.sessions.flash_logged_out":"Outras sessões foram encerradas.","settings.authentication.password.update_title":"Atualizar senha","settings.authentication.password.set_title":"Definir uma senha","settings.authentication.password.update_description":"Use uma senha longa e aleatória para manter sua conta segura.","settings.authentication.password.set_description":"Adicione uma senha para entrar sem precisar de um provedor conectado.","settings.authentication.password.current_password":"Senha atual","settings.authentication.password.new_password":"Nova senha","settings.authentication.password.confirm_password":"Confirmar senha","settings.authentication.password.save":"Salvar senha","settings.authentication.password.set":"Definir senha","settings.authentication.providers.title":"Contas conectadas","settings.authentication.providers.description":"Faça login mais rápido usando esses provedores conectados.","settings.authentication.providers.connected":"Conectada","settings.authentication.providers.not_connected":"Não conectada","settings.authentication.providers.connect":"Conectar","settings.authentication.providers.disconnect":"Desconectar","settings.authentication.providers.flash_disconnected":":provider desconectada com sucesso.","settings.authentication.providers.flash_connected":":provider conectada com sucesso.","settings.authentication.providers.flash_already_linked":"Essa conta do :provider já está vinculada a outro usuário.","settings.authentication.providers.flash_cannot_disconnect":"Você não pode desconectar seu único método de login. Defina uma senha ou conecte outro provedor primeiro.","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_password":"Uma vez excluída, todos os seus recursos e dados também serão permanentemente removidos. Digite sua senha para confirmar.","settings.delete_account.modal_description_email":"Uma vez excluída, todos os seus recursos e dados também serão permanentemente removidos. Digite o seu e-mail :email para confirmar.","settings.delete_account.password":"Senha","settings.delete_account.password_placeholder":"Senha","settings.delete_account.email_placeholder":"Seu e-mail","settings.delete_account.email_mismatch":"O e-mail não corresponde à sua conta.","settings.delete_account.cancel":"Cancelar","settings.delete_account.confirm":"Excluir conta","settings.workspace.tabs.workspace":"Workspace","settings.workspace.tabs.brand":"Marca","settings.workspace.tabs.users":"Membros","settings.workspace.tabs.api_keys":"API Keys","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.name":"Nome do workspace","settings.brand.name_placeholder":"Minha marca","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.brand_color":"Cor da marca","settings.brand.background_color":"Cor de fundo","settings.brand.text_color":"Cor do texto","settings.brand.font":"Fonte","settings.brand.image_style":"Estilo das imagens","settings.brand.image_style_description":"Estilo visual aplicado ao gerar imagens de slides e capas para posts com AI.","settings.brand.image_style_cinematic":"Cinematográfico","settings.brand.image_style_illustration":"Ilustração","settings.brand.image_style_isometric_3d":"Isométrico","settings.brand.image_style_cartoon":"Cartoon","settings.brand.image_style_typographic":"Tipográfico","settings.brand.image_style_infographic":"Infográfico","settings.brand.image_style_minimalist":"Minimalista","settings.brand.image_style_mockup":"Mockup","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.roles.viewer":"Visualizador","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.account.tabs.account":"Conta","settings.account.tabs.usage":"Uso","settings.account.tabs.billing":"Faturamento","settings.account.title":"Configurações da conta","settings.account.description":"Gerencie o nome da conta e o e-mail de cobrança","settings.account.name":"Nome da conta","settings.account.name_placeholder":"Minha Empresa","settings.account.billing_email":"E-mail de cobrança","settings.account.billing_email_placeholder":"cobranca@empresa.com","settings.account.billing_email_hint":"Este e-mail será usado para faturas e comunicações de cobrança do Stripe.","settings.account.submit":"Salvar","settings.flash.account_updated":"Conta atualizada com sucesso!","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.signatures":"Assinaturas","sidebar.workspace.labels":"Etiquetas","sidebar.workspace.assets":"Mídias","sidebar.workspace.api_keys":"API Keys","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.support":"Suporte","sidebar.analytics":"Analytics","sidebar.settings":"Configurações","sidebar.posts.calendar":"Calendário","sidebar.posts.all":"Todos","sidebar.posts.scheduled":"Agendados","sidebar.posts.posted":"Publicados","sidebar.posts.drafts":"Rascunhos","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","signatures.title":"Assinaturas","signatures.description":"Crie assinaturas reutilizáveis pra anexar rapidamente nos seus posts","signatures.search":"Buscar assinaturas...","signatures.new":"Nova assinatura","signatures.empty_title":"Nenhuma assinatura ainda","signatures.empty_description":"Crie assinaturas pra anexar hashtags, links ou qualquer texto reutilizável nos seus posts","signatures.no_search_results":"Nenhuma assinatura corresponde à busca","signatures.try_different_search":"Tente outra palavra-chave ou limpe a busca.","signatures.table.name":"Nome","signatures.table.content":"Conteúdo","signatures.table.created_at":"Criado em","signatures.actions.edit":"Editar assinatura","signatures.actions.delete":"Excluir assinatura","signatures.create.title":"Criar assinatura","signatures.create.description":"Dê um nome à sua assinatura e o conteúdo pra anexar (hashtags, links, texto livre — o que você reutiliza).","signatures.create.name":"Nome","signatures.create.name_placeholder":"ex: Marketing, Viagem, Encerramento da marca","signatures.create.content":"Conteúdo","signatures.create.content_placeholder":"#marketing #socialmedia\nSaiba mais: https://suamarca.com","signatures.create.content_hint":"Hashtags, links, intros, assinaturas — qualquer coisa que você anexa nos posts.","signatures.create.submit":"Criar assinatura","signatures.create.submitting":"Criando...","signatures.edit.title":"Editar assinatura","signatures.edit.description":"Atualize o nome e o conteúdo desta assinatura.","signatures.edit.name":"Nome","signatures.edit.name_placeholder":"ex: Marketing, Viagem, Encerramento da marca","signatures.edit.content":"Conteúdo","signatures.edit.content_placeholder":"#marketing #socialmedia\nSaiba mais: https://suamarca.com","signatures.edit.content_hint":"Hashtags, links, intros, assinaturas — qualquer coisa que você anexa nos posts.","signatures.edit.submit":"Salvar alterações","signatures.edit.submitting":"Salvando...","signatures.delete.title":"Deletar assinatura","signatures.delete.description":"Tem certeza que quer deletar esta assinatura? Esta ação não pode ser desfeita.","signatures.delete.confirm":"Deletar","signatures.delete.cancel":"Cancelar","signatures.flash.created":"Assinatura criada.","signatures.flash.updated":"Assinatura atualizada.","signatures.flash.deleted":"Assinatura deletada.","usage.title":"Uso","usage.section_account":"Conta","usage.section_account_description":"Cotas e limites do seu plano :plan.","usage.section_ai":"Créditos AI","usage.section_ai_description":"Os créditos são debitados conforme você usa os recursos de AI. Eles são renovados no dia 1 de cada mês.","usage.workspaces":"Workspaces","usage.social_accounts":"Contas Sociais","usage.members":"Membros","usage.credits":"Créditos","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 um pouco sobre você ou seu projeto. Vamos usar pra personalizar os 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.brand_color":"Cor da marca","workspaces.create.background_color":"Cor de fundo","workspaces.create.text_color":"Cor do texto","workspaces.create.submit":"Criar workspace","workspaces.create.success":"Workspace criado. Conecte uma conta social para começar a postar.","workspaces.limit_reached":"Você atingiu o limite de workspaces do seu plano.","workspaces.flash.deleted":"Workspace excluído com sucesso."} \ No newline at end of file diff --git a/lang/pt-BR/posts.php b/lang/pt-BR/posts.php index e36b4744..71b5c515 100644 --- a/lang/pt-BR/posts.php +++ b/lang/pt-BR/posts.php @@ -175,6 +175,7 @@ 'draft' => 'Rascunho', 'scheduled' => 'Agendado', 'publishing' => 'Publicando', + 'retrying' => 'Tentando novamente', 'published' => 'Publicado', 'partially_published' => 'Parcialmente Publicado', 'failed' => 'Falhou', @@ -323,6 +324,7 @@ 'scheduled' => 'Agendado', 'published' => 'Publicado', 'publishing' => 'Publicando...', + 'retrying' => 'Tentando novamente...', 'failed' => 'Falhou', ], diff --git a/resources/js/composables/usePostStatus.ts b/resources/js/composables/usePostStatus.ts index 401c5f81..b381d779 100644 --- a/resources/js/composables/usePostStatus.ts +++ b/resources/js/composables/usePostStatus.ts @@ -19,6 +19,7 @@ const CONFIGS: Record> = { draft: { variant: 'outline', icon: IconFileText }, scheduled: { variant: 'default', icon: IconClock }, publishing: { variant: 'warning', icon: IconLoader2 }, + retrying: { variant: 'warning', icon: IconLoader2 }, published: { variant: 'success', icon: IconCircleCheck }, partially_published: { variant: 'warning', icon: IconAlertCircle }, failed: { variant: 'destructive', icon: IconAlertCircle }, @@ -33,6 +34,7 @@ export const getPlatformStatusConfig = (status: string): StatusConfig => { const map: Record = { pending: 'draft', publishing: 'publishing', + retrying: 'retrying', published: 'published', failed: 'failed', }; diff --git a/tests/Feature/Jobs/PublishToSocialPlatformTest.php b/tests/Feature/Jobs/PublishToSocialPlatformTest.php index ecedf7af..863bf89f 100644 --- a/tests/Feature/Jobs/PublishToSocialPlatformTest.php +++ b/tests/Feature/Jobs/PublishToSocialPlatformTest.php @@ -7,9 +7,9 @@ use App\Enums\SocialAccount\Status as AccountStatus; use App\Enums\UserWorkspace\Role; use App\Events\PostPlatformStatusUpdated; +use App\Exceptions\PlatformUnavailableException; use App\Exceptions\Social\ErrorCategory; use App\Exceptions\Social\LinkedInPublishException; -use App\Exceptions\PlatformUnavailableException; use App\Exceptions\TokenExpiredException; use App\Jobs\PublishToSocialPlatform; use App\Jobs\SendNotification; @@ -20,6 +20,7 @@ use App\Models\Workspace; use App\Services\Social\ConnectionVerifier; use App\Services\Social\LinkedInPublisher; +use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Queue; @@ -110,14 +111,42 @@ expect($this->socialAccount->status)->toBe(AccountStatus::TokenExpired); }); -test('publish does NOT mark account expired when retry refresh hits platform unavailable', function () { +test('publish reschedules platform unavailable retry via Bus dispatch (not marked Failed, not expired)', function () { + Bus::fake([PublishToSocialPlatform::class]); + Event::fake(); + Mail::fake(); + + $publisher = Mockery::mock(LinkedInPublisher::class); + $publisher->shouldReceive('publish')->andThrow( + new PlatformUnavailableException('LinkedIn API returned 503 during token refresh', 503) + ); + + $this->app->instance(LinkedInPublisher::class, $publisher); + + (new PublishToSocialPlatform($this->postPlatform))->handle(); + + $this->postPlatform->refresh(); + $this->socialAccount->refresh(); + + expect($this->postPlatform->status)->toBe(PlatformStatus::Retrying); + expect($this->postPlatform->error_context['category'] ?? null)->toBe('platform_unavailable'); + expect($this->postPlatform->error_context['http_status'] ?? null)->toBe(503); + expect($this->postPlatform->error_context['retry_count'] ?? null)->toBe(1); + expect($this->socialAccount->status)->toBe(AccountStatus::Connected); + + Bus::assertDispatched(PublishToSocialPlatform::class, function ($job) { + return $job->postPlatform->id === $this->postPlatform->id; + }); +}); + +test('publish reschedules retry when retry-refresh path hits platform unavailable', function () { + Bus::fake([PublishToSocialPlatform::class]); Event::fake(); Mail::fake(); // Publisher first throws TokenExpired (401-style), the retry-refresh // path goes through ConnectionVerifier::verify which can in turn raise - // PlatformUnavailable if the platform is down. The account must stay - // Connected — it was the platform that failed, not the token. + // PlatformUnavailable if the platform is down. $publisher = Mockery::mock(LinkedInPublisher::class); $publisher->shouldReceive('publish')->andThrow(new TokenExpiredException('Token expired', '401')); @@ -134,32 +163,34 @@ $this->postPlatform->refresh(); $this->socialAccount->refresh(); - expect($this->postPlatform->status)->toBe(PlatformStatus::Failed); + expect($this->postPlatform->status)->toBe(PlatformStatus::Retrying); expect($this->postPlatform->error_context['category'] ?? null)->toBe('platform_unavailable'); - expect($this->postPlatform->error_context['http_status'] ?? null)->toBe(503); expect($this->socialAccount->status)->toBe(AccountStatus::Connected); + + Bus::assertDispatched(PublishToSocialPlatform::class); }); -test('publish to social platform does NOT mark account expired when platform is unavailable', function () { +test('publish retry count increments across successive platform_unavailable attempts', function () { + Bus::fake([PublishToSocialPlatform::class]); Event::fake(); Mail::fake(); $publisher = Mockery::mock(LinkedInPublisher::class); $publisher->shouldReceive('publish')->andThrow( - new PlatformUnavailableException('LinkedIn API returned 503 during token refresh', 503) + new PlatformUnavailableException('LinkedIn 503', 503) ); - $this->app->instance(LinkedInPublisher::class, $publisher); + // Simulate prior attempts + $this->postPlatform->update([ + 'error_context' => ['retry_count' => 5], + ]); + (new PublishToSocialPlatform($this->postPlatform))->handle(); $this->postPlatform->refresh(); - $this->socialAccount->refresh(); - expect($this->postPlatform->status)->toBe(PlatformStatus::Failed); - expect($this->postPlatform->error_context['category'] ?? null)->toBe('platform_unavailable'); - expect($this->postPlatform->error_context['http_status'] ?? null)->toBe(503); - expect($this->socialAccount->status)->toBe(AccountStatus::Connected); + expect($this->postPlatform->error_context['retry_count'] ?? null)->toBe(6); }); test('publish to social platform updates post status when all platforms finished', function () { From 2509b6ee265053abefd7b6fb7c4272fb2ac6884f Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 19 May 2026 10:14:49 -0300 Subject: [PATCH 14/14] test(social): plug remaining gaps around retry-reschedule behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit found three behaviors with no explicit assertion: - Job is dispatched with a 10-minute delay (would silently regress if the duration changed). Uses Carbon::setTestNow + Bus::assertDispatched inspecting \$job->delay. - error_context.last_attempt_at is recorded at the moment of failure. - updatePostStatus does NOT finalize the parent Post while any of its platforms is in Retrying — covers the central invariant of the feature (the post must stay Publishing until every platform lands in Published or Failed). - A platform currently in Retrying transitions to Published when the next attempt succeeds — proves the loop terminates. --- .../Jobs/PublishToSocialPlatformTest.php | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/tests/Feature/Jobs/PublishToSocialPlatformTest.php b/tests/Feature/Jobs/PublishToSocialPlatformTest.php index 863bf89f..92abd65a 100644 --- a/tests/Feature/Jobs/PublishToSocialPlatformTest.php +++ b/tests/Feature/Jobs/PublishToSocialPlatformTest.php @@ -20,6 +20,7 @@ use App\Models\Workspace; use App\Services\Social\ConnectionVerifier; use App\Services\Social\LinkedInPublisher; +use Carbon\Carbon; use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Mail; @@ -170,6 +171,125 @@ Bus::assertDispatched(PublishToSocialPlatform::class); }); +test('publish reschedules retry exactly 10 minutes into the future', function () { + Bus::fake([PublishToSocialPlatform::class]); + Event::fake(); + Mail::fake(); + + $now = now()->startOfMinute(); + Carbon::setTestNow($now); + + $publisher = Mockery::mock(LinkedInPublisher::class); + $publisher->shouldReceive('publish')->andThrow( + new PlatformUnavailableException('LinkedIn 503', 503) + ); + $this->app->instance(LinkedInPublisher::class, $publisher); + + (new PublishToSocialPlatform($this->postPlatform))->handle(); + + $this->postPlatform->refresh(); + + // error_context tracks the next attempt — must be exactly +10 min + expect($this->postPlatform->error_context['next_attempt_at'] ?? null) + ->toBe($now->copy()->addMinutes(10)->toIso8601String()); + + // The actual dispatched job carries the same delay + Bus::assertDispatched(PublishToSocialPlatform::class, function ($job) use ($now) { + // $job->delay is a Carbon|DateInterval|int set by ->delay(...) + $delayAt = $job->delay instanceof DateTimeInterface + ? Carbon::instance($job->delay) + : null; + + return $delayAt !== null + && $delayAt->equalTo($now->copy()->addMinutes(10)); + }); + + Carbon::setTestNow(); +}); + +test('publish records last_attempt_at when rescheduling for retry', function () { + Bus::fake([PublishToSocialPlatform::class]); + Event::fake(); + Mail::fake(); + + $now = now()->startOfMinute(); + Carbon::setTestNow($now); + + $publisher = Mockery::mock(LinkedInPublisher::class); + $publisher->shouldReceive('publish')->andThrow( + new PlatformUnavailableException('LinkedIn 503', 503) + ); + $this->app->instance(LinkedInPublisher::class, $publisher); + + (new PublishToSocialPlatform($this->postPlatform))->handle(); + + $this->postPlatform->refresh(); + + expect($this->postPlatform->error_context['last_attempt_at'] ?? null) + ->toBe($now->toIso8601String()); + + Carbon::setTestNow(); +}); + +test('post stays in Publishing while one platform is still Retrying', function () { + Bus::fake([PublishToSocialPlatform::class]); + Event::fake(); + Mail::fake(); + + // Second LinkedIn account on the same post — first one will publish OK, + // second one will hit PlatformUnavailable and reschedule. + $secondAccount = SocialAccount::factory()->linkedin()->create(['workspace_id' => $this->workspace->id]); + $secondPlatform = PostPlatform::factory()->linkedin()->create([ + 'post_id' => $this->post->id, + 'social_account_id' => $secondAccount->id, + 'enabled' => true, + 'status' => PlatformStatus::Published, // simulate already published + 'platform_post_id' => 'sibling-123', + ]); + + // Start the post as Publishing so updatePostStatus sees the in-flight context + $this->post->update(['status' => PostStatus::Publishing]); + + $publisher = Mockery::mock(LinkedInPublisher::class); + $publisher->shouldReceive('publish')->andThrow( + new PlatformUnavailableException('LinkedIn 503', 503) + ); + $this->app->instance(LinkedInPublisher::class, $publisher); + + (new PublishToSocialPlatform($this->postPlatform))->handle(); + + $this->postPlatform->refresh(); + $this->post->refresh(); + + expect($this->postPlatform->status)->toBe(PlatformStatus::Retrying); + // Post is NOT finalized because one of its platforms is still pending retry. + expect($this->post->status)->toBe(PostStatus::Publishing); +}); + +test('successful publish after a retry transitions the platform to Published', function () { + // Pre-condition: this platform already failed once and is currently Retrying. + $this->postPlatform->update([ + 'status' => PlatformStatus::Retrying, + 'error_context' => ['retry_count' => 3, 'category' => 'platform_unavailable'], + ]); + + Event::fake(); + + $publisher = Mockery::mock(LinkedInPublisher::class); + $publisher->shouldReceive('publish')->andReturn([ + 'id' => 'post-after-retry', + 'url' => 'https://linkedin.com/post/after-retry', + ]); + $this->app->instance(LinkedInPublisher::class, $publisher); + + (new PublishToSocialPlatform($this->postPlatform))->handle(); + + $this->postPlatform->refresh(); + + expect($this->postPlatform->status)->toBe(PlatformStatus::Published); + expect($this->postPlatform->platform_post_id)->toBe('post-after-retry'); +}); + test('publish retry count increments across successive platform_unavailable attempts', function () { Bus::fake([PublishToSocialPlatform::class]); Event::fake();