From e3538df2f0652e58d305a4b2cebd677c6975e03d Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 7 May 2026 09:28:02 -0300 Subject: [PATCH 01/15] feat: end-to-end PostHog tracking with reactive group metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire PostHog identify + dual-group context across the stack so events land on the right person and account/workspace groups, with counts kept fresh by Inertia navigations rather than per-domain triggers. - New `app/Jobs/SyncUserToPostHog.php` (queue `posthog`): centralised high-level sync — identifies the user and group-identifies their account + current workspace using `$account->usage()` so the metrics reuse the same source of truth Inertia ships in shared props. - `app/Actions/User/CreateUser.php`: dispatches `SyncUserToPostHog` on signup instead of calling `PostHogService` inline. Keeps the action fast and routes everything through the queue. - `app/Listeners/StripeEventListener.php`: webhook now captures `subscription.created`/`updated`/`cancelled` against the account owner profile (with `account` group auto-attached) and re-dispatches `SyncUserToPostHog` so plan/has_active_subscription/is_on_trial refresh after Stripe state changes. - `app/Services/PostHogService.php`: `capture()` accepts an optional `Account` that auto-attaches `$groups.account`, `account_id`, and `plan` properties. Each public method short-circuits when `POSTHOG_API_KEY` is unset so self-hosted installs are unaffected. - `app/Models/Traits/HasUsage.php`: adds `postCount` to the usage shape (combined `withCount(['socialAccounts','posts'])` query) so posts count is part of the same payload Inertia already ships. - `config/horizon.php`: adds `posthog` to `supervisor-1` queues so the queued PostHog jobs actually drain in production. - `resources/js/app.ts`: extracts `syncPostHogContext(page)` and calls it on boot AND on every Inertia navigation, reading the fresh `usage` props. This: - Refreshes account group counts (workspaces, social accounts, posts, members, credits) without per-domain triggers. - Resolves the workspace-switch case where `setup()` does not re-run but `navigate` fires with the new `auth.currentWorkspace`. - Captures the initial `$pageview` so the first page of a session is no longer dropped. - `resources/js/components/UserMenuContent.vue`: `posthog.reset()` on logout so a follow-up login on the same browser doesn't keep events attributed to the previous user. - `resources/js/composables/useFeatureAccess.ts`: TS `Usage` interface gains `postCount`. - `tests/Feature/Models/HasUsageTraitTest.php`: updated for the new usage shape. Full suite: 1407 passed. Hierarchy aligned with the domain model: person = User, group `account` = billing/plan parent, group `workspace` = collaboration child (carries `account_id` for drill-down). --- app/Actions/User/CreateUser.php | 7 +- app/Jobs/SyncUserToPostHog.php | 87 ++++++++++++++++++++ app/Listeners/StripeEventListener.php | 60 ++++++++++++-- app/Models/Traits/HasUsage.php | 14 ++-- app/Services/PostHogService.php | 35 ++++++-- config/horizon.php | 2 +- resources/js/app.ts | 63 +++++++++++--- resources/js/components/UserMenuContent.vue | 2 + resources/js/composables/useFeatureAccess.ts | 1 + tests/Feature/Models/HasUsageTraitTest.php | 1 + 10 files changed, 238 insertions(+), 34 deletions(-) create mode 100644 app/Jobs/SyncUserToPostHog.php diff --git a/app/Actions/User/CreateUser.php b/app/Actions/User/CreateUser.php index f2b4f7e0..6bb253c1 100644 --- a/app/Actions/User/CreateUser.php +++ b/app/Actions/User/CreateUser.php @@ -4,6 +4,7 @@ namespace App\Actions\User; +use App\Jobs\SyncUserToPostHog; use App\Models\Account; use App\Models\User; use Illuminate\Support\Facades\DB; @@ -16,7 +17,7 @@ class CreateUser */ public static function execute(array $data, array $utmParameters = []): User { - return DB::transaction(function () use ($data, $utmParameters): User { + $user = DB::transaction(function () use ($data, $utmParameters): User { $isInviteRegistration = data_get($data, 'is_invite', false); $account = Account::create([ @@ -39,5 +40,9 @@ public static function execute(array $data, array $utmParameters = []): User return $user; }); + + SyncUserToPostHog::dispatch((string) $user->id); + + return $user; } } diff --git a/app/Jobs/SyncUserToPostHog.php b/app/Jobs/SyncUserToPostHog.php new file mode 100644 index 00000000..f9b6fb52 --- /dev/null +++ b/app/Jobs/SyncUserToPostHog.php @@ -0,0 +1,87 @@ +onQueue('posthog'); + } + + public function handle(PostHogService $postHog): void + { + $user = User::with(['account.plan', 'currentWorkspace'])->find($this->userId); + + if (! $user) { + return; + } + + $postHog->identify((string) $user->id, [ + 'email' => $user->email, + 'name' => $user->name, + '$set_once' => ['signed_up_at' => $user->created_at?->toIso8601String()], + ]); + + if ($account = $user->account) { + $usage = $account->usage(); + + $postHog->groupIdentify('account', (string) $account->id, [ + 'name' => $account->name, + 'plan' => $account->plan?->name, + 'plan_slug' => $account->plan?->slug, + 'has_active_subscription' => $account->hasActiveSubscription(), + 'is_on_trial' => $account->isOnTrial(), + 'workspaces_count' => $usage['workspaceCount'], + 'members_count' => $usage['memberCount'], + 'social_accounts_count' => $usage['socialAccountCount'], + 'posts_count' => $usage['postCount'], + 'pending_invites_count' => $usage['pendingInviteCount'], + 'credits_used' => $usage['creditsUsed'], + 'created_at' => $account->created_at?->toIso8601String(), + ]); + } + + if ($workspace = $user->currentWorkspace) { + $postHog->groupIdentify('workspace', (string) $workspace->id, [ + 'name' => $workspace->name, + 'account_id' => (string) $workspace->account_id, + 'social_accounts_count' => $workspace->socialAccounts()->count(), + 'posts_count' => $workspace->posts()->count(), + 'created_at' => $workspace->created_at?->toIso8601String(), + ]); + } + } +} diff --git a/app/Listeners/StripeEventListener.php b/app/Listeners/StripeEventListener.php index 62d75263..38e2b5a7 100644 --- a/app/Listeners/StripeEventListener.php +++ b/app/Listeners/StripeEventListener.php @@ -5,7 +5,9 @@ namespace App\Listeners; use App\Events\SubscriptionCreated; +use App\Jobs\SyncUserToPostHog; use App\Models\Account; +use App\Services\PostHogService; use Illuminate\Support\Facades\Log; use Laravel\Cashier\Events\WebhookReceived; @@ -21,16 +23,16 @@ public function handle(WebhookReceived $event): void return; } - $workspace = Account::where('stripe_id', $stripeCustomerId)->first(); + $account = Account::where('stripe_id', $stripeCustomerId)->first(); - if (! $workspace) { + if (! $account) { return; } match ($type) { - 'customer.subscription.created' => $this->handleSubscriptionCreated($workspace, $event->payload), - 'customer.subscription.updated' => $this->handleSubscriptionUpdated($workspace, $event->payload), - 'customer.subscription.deleted' => $this->handleSubscriptionDeleted($workspace, $event->payload), + 'customer.subscription.created' => $this->handleSubscriptionCreated($account, $event->payload), + 'customer.subscription.updated' => $this->handleSubscriptionUpdated($account, $event->payload), + 'customer.subscription.deleted' => $this->handleSubscriptionDeleted($account, $event->payload), default => null, }; } catch (\Exception $e) { @@ -41,18 +43,58 @@ public function handle(WebhookReceived $event): void } } + /** + * @param array $payload + */ protected function handleSubscriptionCreated(Account $account, array $payload): void { SubscriptionCreated::dispatch($account); + + $this->trackBilling($account, 'subscription.created', $payload); } - protected function handleSubscriptionUpdated(Account $workspace, array $payload): void + /** + * @param array $payload + */ + protected function handleSubscriptionUpdated(Account $account, array $payload): void { - // + $this->trackBilling($account, 'subscription.updated', $payload); } - protected function handleSubscriptionDeleted(Account $workspace, array $payload): void + /** + * @param array $payload + */ + protected function handleSubscriptionDeleted(Account $account, array $payload): void { - // + $this->trackBilling($account, 'subscription.cancelled', $payload); + } + + /** + * Capture the lifecycle event on the account owner's profile and trigger + * a fresh `SyncUserToPostHog` so the account group properties (plan, + * has_active_subscription, is_on_trial) reflect the new Stripe state. + * + * @param array $payload + */ + private function trackBilling(Account $account, string $event, array $payload): void + { + if (! $account->owner_id) { + return; + } + + $properties = [ + 'stripe_status' => data_get($payload, 'data.object.status'), + 'plan' => $account->plan?->name, + 'plan_slug' => $account->plan?->slug, + ]; + + app(PostHogService::class)->capture( + (string) $account->owner_id, + $event, + $properties, + $account, + ); + + SyncUserToPostHog::dispatch((string) $account->owner_id); } } diff --git a/app/Models/Traits/HasUsage.php b/app/Models/Traits/HasUsage.php index 3cf3e351..b974e855 100644 --- a/app/Models/Traits/HasUsage.php +++ b/app/Models/Traits/HasUsage.php @@ -22,20 +22,22 @@ trait HasUsage { /** - * @return array{workspaceCount: int, socialAccountCount: int, memberCount: int, pendingInviteCount: int, creditsUsed: int} + * @return array{workspaceCount: int, socialAccountCount: int, memberCount: int, pendingInviteCount: int, postCount: int, creditsUsed: int} */ public function usage(): array { + $workspaces = $this->workspaces() + ->withCount(['socialAccounts', 'posts']) + ->get(); + return [ - 'workspaceCount' => $this->workspaces()->count(), - 'socialAccountCount' => $this->workspaces() - ->withCount('socialAccounts') - ->get() - ->sum('social_accounts_count'), + 'workspaceCount' => $workspaces->count(), + 'socialAccountCount' => (int) $workspaces->sum('social_accounts_count'), 'memberCount' => $this->users()->count(), 'pendingInviteCount' => Invite::where('account_id', $this->id) ->whereNull('accepted_at') ->count(), + 'postCount' => (int) $workspaces->sum('posts_count'), 'creditsUsed' => AiUsageLog::monthlyCredits($this->id), ]; } diff --git a/app/Services/PostHogService.php b/app/Services/PostHogService.php index 60d0dcc2..5c23b457 100644 --- a/app/Services/PostHogService.php +++ b/app/Services/PostHogService.php @@ -5,20 +5,37 @@ namespace App\Services; use App\Jobs\SendPostHogEvent; +use App\Models\Account; use Illuminate\Support\Facades\Log; class PostHogService { /** + * Capture an event for the given distinct id. When `$account` is supplied, + * the workspace/plan group is auto-attached so the event is filterable in + * PostHog by `$groups.account` and the `account_id` / `plan` properties. + * * @param array $properties */ - public function capture(string $distinctId, string $event, array $properties = []): void + public function capture(string $distinctId, string $event, array $properties = [], ?Account $account = null): void { - $this->dispatch('capture', [ + if (! config('services.posthog.api_key')) { + return; + } + + $payload = [ 'distinctId' => $distinctId, 'event' => $event, 'properties' => $properties, - ]); + ]; + + if ($account) { + $payload['properties']['$groups'] = ['account' => (string) $account->id]; + $payload['properties']['account_id'] = (string) $account->id; + $payload['properties']['plan'] = $account->plan?->name; + } + + $this->dispatch('capture', $payload); } /** @@ -26,6 +43,10 @@ public function capture(string $distinctId, string $event, array $properties = [ */ public function identify(string $distinctId, array $properties = []): void { + if (! config('services.posthog.api_key')) { + return; + } + $this->dispatch('identify', [ 'distinctId' => $distinctId, 'properties' => $properties, @@ -37,6 +58,10 @@ public function identify(string $distinctId, array $properties = []): void */ public function groupIdentify(string $groupType, string $groupKey, array $properties = []): void { + if (! config('services.posthog.api_key')) { + return; + } + $this->dispatch('groupIdentify', [ 'groupType' => $groupType, 'groupKey' => $groupKey, @@ -49,10 +74,6 @@ public function groupIdentify(string $groupType, string $groupKey, array $proper */ private function dispatch(string $method, array $payload): void { - if (! config('services.posthog.api_key')) { - return; - } - try { SendPostHogEvent::dispatch([ ['method' => $method, 'payload' => $payload], diff --git a/config/horizon.php b/config/horizon.php index 3071d096..60ede02f 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -214,7 +214,7 @@ 'defaults' => [ 'supervisor-1' => [ 'connection' => 'redis', - 'queue' => ['default'], + 'queue' => ['default', 'posthog'], 'balance' => 'auto', 'autoScalingStrategy' => 'time', 'maxProcesses' => 1, diff --git a/resources/js/app.ts b/resources/js/app.ts index fdf022c4..231f3389 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -1,6 +1,7 @@ import '../css/app.css'; import { createInertiaApp, router } from '@inertiajs/vue3'; +import type { Page } from '@inertiajs/core'; import { configureEcho } from '@laravel/echo-vue'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import { i18nVue } from 'laravel-vue-i18n'; @@ -12,12 +13,51 @@ import dayjs from './dayjs'; import posthog from './posthog'; import type { Auth } from './types'; +interface Usage { + workspaceCount: number; + socialAccountCount: number; + memberCount: number; + pendingInviteCount: number; + postCount: number; + creditsUsed: number; +} + configureEcho({ broadcaster: 'reverb', }); const appName = import.meta.env.VITE_APP_NAME || 'TryPost.it'; +// Re-applies the PostHog account/workspace group context with fresh metrics. +// Called on every Inertia navigation so `usage` counts (workspaces, social +// accounts, posts, members) stay reactive — the backend already ships these +// counts on every Inertia request, so this leverages props that are already +// loaded without firing extra queries. +const syncPostHogContext = (page: Page): void => { + const auth = page.props.auth as Auth | undefined; + const usage = page.props.usage as Usage | null; + + if (!auth?.user) return; + + if (auth.account) { + posthog.group('account', auth.account.id, { + name: auth.account.name, + workspaces_count: usage?.workspaceCount, + social_accounts_count: usage?.socialAccountCount, + members_count: usage?.memberCount, + posts_count: usage?.postCount, + credits_used: usage?.creditsUsed, + }); + } + + if (auth.currentWorkspace) { + posthog.group('workspace', auth.currentWorkspace.id, { + name: auth.currentWorkspace.name, + account_id: auth.account?.id, + }); + } +}; + createInertiaApp({ title: (title) => (title ? `${title} - ${appName}` : appName), resolve: (name) => @@ -49,18 +89,21 @@ createInertiaApp({ $email: auth.user.email, $name: auth.user.name, }); - - if (auth.currentWorkspace) { - posthog.group('workspace', auth.currentWorkspace.id, { - name: auth.currentWorkspace.name, - }); - } } - router.on('navigate', () => { - posthog.capture('$pageview', { - $current_url: window.location.href, - }); + // Initial group context + initial pageview. The backend mirrors this + // hierarchy in app/Jobs/SyncUserToPostHog.php so events emitted from + // the server land on the same person + group identifiers. + syncPostHogContext(props.initialPage); + posthog.capture('$pageview', { $current_url: window.location.href }); + + router.on('navigate', (event) => { + // Re-sync group context on every navigation: refreshes the count + // metrics on the `account` group AND covers workspace switches + // (which update auth.currentWorkspace via Inertia's prop refresh + // without triggering setup() again). + syncPostHogContext(event.detail.page); + posthog.capture('$pageview', { $current_url: window.location.href }); }); createApp({ render: () => h(App, props) }) diff --git a/resources/js/components/UserMenuContent.vue b/resources/js/components/UserMenuContent.vue index 5691101f..008964a4 100644 --- a/resources/js/components/UserMenuContent.vue +++ b/resources/js/components/UserMenuContent.vue @@ -23,6 +23,7 @@ import { } from '@/components/ui/dropdown-menu'; import UserInfo from '@/components/UserInfo.vue'; import dayjs from '@/dayjs'; +import posthog from '@/posthog'; import { logout } from '@/routes'; import { edit } from '@/routes/app/profile'; import type { User } from '@/types'; @@ -59,6 +60,7 @@ const switchLanguage = (code: string) => { }; const handleLogout = () => { + posthog.reset(); router.flushAll(); }; diff --git a/resources/js/composables/useFeatureAccess.ts b/resources/js/composables/useFeatureAccess.ts index eb070478..20b00ffb 100644 --- a/resources/js/composables/useFeatureAccess.ts +++ b/resources/js/composables/useFeatureAccess.ts @@ -12,6 +12,7 @@ interface Usage { socialAccountCount: number; memberCount: number; pendingInviteCount: number; + postCount: number; creditsUsed: number; } diff --git a/tests/Feature/Models/HasUsageTraitTest.php b/tests/Feature/Models/HasUsageTraitTest.php index 4421bd9c..e26d0d57 100644 --- a/tests/Feature/Models/HasUsageTraitTest.php +++ b/tests/Feature/Models/HasUsageTraitTest.php @@ -36,6 +36,7 @@ 'socialAccountCount' => 3, 'memberCount' => 3, 'pendingInviteCount' => 2, + 'postCount' => 0, 'creditsUsed' => 0, ]); }); From 3e3c0b4c9d07f69102fac5dd50637243f7499caf Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 7 May 2026 09:41:36 -0300 Subject: [PATCH 02/15] refactor: namespace PostHog jobs + extract billing tracking, add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorganises PostHog plumbing under `App\Jobs\PostHog` and extracts the Stripe billing capture out of `StripeEventListener` into its own job. Adds the missing test coverage that was promised but not delivered in the previous commit. Code changes: - Move `app/Jobs/SendPostHogEvent.php` → `app/Jobs/PostHog/SendEvent.php` (low-level dispatcher). - Move `app/Jobs/SyncUserToPostHog.php` → `app/Jobs/PostHog/SyncUser.php` (high-level user/account/workspace sync). - New `app/Jobs/PostHog/TrackBilling.php` that owns the capture('subscription.*') + SyncUser re-dispatch flow. Receives account id + event name + payload, runs on the `posthog` queue. - `StripeEventListener` slims down to a switch table mapping Stripe event types to PostHog event names and dispatches `TrackBilling`. No more inline tracking logic in the listener. - `resources/js/posthog.ts` now owns `syncPostHogContext(page)` and `capturePageview()`. `resources/js/app.ts` imports them — no behaviour inlined in the bootstrap. - `app/Services/PostHogService.php` and `app/Actions/User/CreateUser.php` updated to the new namespaces. Tests added/updated: - `tests/Feature/Jobs/PostHog/SyncUserTest.php` — identify/group payload shape, account metrics, workspace skip when none, queue assignment, no-op without api key. - `tests/Feature/Jobs/PostHog/TrackBillingTest.php` — capture payload, SyncUser re-dispatch, missing-account/owner handling, api key gate. - `tests/Feature/Jobs/PostHog/SendEventTest.php` — moved from `tests/Feature/SendPostHogEventTest.php` and updated to new namespace. - `tests/Unit/PostHogServiceTest.php` — adds coverage for the account-aware capture (auto-attached `\$groups.account`, `account_id`, `plan`) and the no-account branch. - `tests/Feature/Listeners/StripeEventListenerTest.php` — replaces the old inline-PostHog assertions with `Bus::fake([TrackBilling::class])` and verifies the listener dispatches TrackBilling with the right account id + event name for each subscription type, and skips non-subscription event types. - `tests/Feature/Actions/User/CreateUserTest.php` — verifies signup dispatches `SyncUser` with the new user id. Suite: 1427 passed (+20 net new, including the previous round of metrics-related tests). --- app/Actions/User/CreateUser.php | 4 +- .../SendEvent.php} | 16 +- .../SyncUser.php} | 8 +- app/Jobs/PostHog/TrackBilling.php | 63 ++++++++ app/Listeners/StripeEventListener.php | 77 ++-------- app/Services/PostHogService.php | 4 +- resources/js/app.ts | 64 +------- resources/js/posthog.ts | 67 +++++++++ tests/Feature/Actions/User/CreateUserTest.php | 17 +++ .../PostHog/SendEventTest.php} | 8 +- tests/Feature/Jobs/PostHog/SyncUserTest.php | 139 ++++++++++++++++++ .../Feature/Jobs/PostHog/TrackBillingTest.php | 98 ++++++++++++ .../Listeners/StripeEventListenerTest.php | 61 +++++++- tests/Unit/PostHogServiceTest.php | 50 ++++++- 14 files changed, 536 insertions(+), 140 deletions(-) rename app/Jobs/{SendPostHogEvent.php => PostHog/SendEvent.php} (66%) rename app/Jobs/{SyncUserToPostHog.php => PostHog/SyncUser.php} (94%) create mode 100644 app/Jobs/PostHog/TrackBilling.php rename tests/Feature/{SendPostHogEventTest.php => Jobs/PostHog/SendEventTest.php} (83%) create mode 100644 tests/Feature/Jobs/PostHog/SyncUserTest.php create mode 100644 tests/Feature/Jobs/PostHog/TrackBillingTest.php diff --git a/app/Actions/User/CreateUser.php b/app/Actions/User/CreateUser.php index 6bb253c1..8ae1e0ca 100644 --- a/app/Actions/User/CreateUser.php +++ b/app/Actions/User/CreateUser.php @@ -4,7 +4,7 @@ namespace App\Actions\User; -use App\Jobs\SyncUserToPostHog; +use App\Jobs\PostHog\SyncUser; use App\Models\Account; use App\Models\User; use Illuminate\Support\Facades\DB; @@ -41,7 +41,7 @@ public static function execute(array $data, array $utmParameters = []): User return $user; }); - SyncUserToPostHog::dispatch((string) $user->id); + SyncUser::dispatch((string) $user->id); return $user; } diff --git a/app/Jobs/SendPostHogEvent.php b/app/Jobs/PostHog/SendEvent.php similarity index 66% rename from app/Jobs/SendPostHogEvent.php rename to app/Jobs/PostHog/SendEvent.php index 59b880dd..0a2ef992 100644 --- a/app/Jobs/SendPostHogEvent.php +++ b/app/Jobs/PostHog/SendEvent.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Jobs; +namespace App\Jobs\PostHog; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -12,7 +12,17 @@ use Illuminate\Support\Facades\Log; use PostHog\PostHog; -class SendPostHogEvent implements ShouldQueue +/** + * Low-level PostHog dispatch worker. Receives an array of pre-built API + * calls (`capture`, `identify`, `groupIdentify`) and forwards them to the + * PostHog SDK. Higher-level jobs in this namespace (`SyncUser`, + * `TrackBilling`) build the payloads and queue this job to do the network + * work. + * + * No-op when `POSTHOG_API_KEY` is unset so self-hosted installs are + * unaffected. + */ +class SendEvent implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -40,7 +50,7 @@ public function handle(): void 'capture' => PostHog::capture(data_get($call, 'payload')), 'identify' => PostHog::identify(data_get($call, 'payload')), 'groupIdentify' => PostHog::groupIdentify(data_get($call, 'payload')), - default => Log::warning('SendPostHogEvent: unknown method', ['method' => data_get($call, 'method')]), + default => Log::warning('PostHog\\SendEvent: unknown method', ['method' => data_get($call, 'method')]), }; } diff --git a/app/Jobs/SyncUserToPostHog.php b/app/Jobs/PostHog/SyncUser.php similarity index 94% rename from app/Jobs/SyncUserToPostHog.php rename to app/Jobs/PostHog/SyncUser.php index f9b6fb52..34ae719a 100644 --- a/app/Jobs/SyncUserToPostHog.php +++ b/app/Jobs/PostHog/SyncUser.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Jobs; +namespace App\Jobs\PostHog; use App\Models\User; use App\Services\PostHogService; @@ -17,8 +17,8 @@ * frontend can't observe (signup, Stripe webhooks) so the person profile * and account/workspace groups carry up-to-date properties without blocking * the calling request. Inertia navigations refresh group counts on the - * client (see `syncPostHogContext` in resources/js/app.ts), so this job is - * not needed on every domain trigger. + * client (see `syncPostHogContext` in resources/js/posthog.ts), so this job + * is not needed on every domain trigger. * * Hierarchy mirrors the domain model: * - person → User @@ -28,7 +28,7 @@ * No-op when POSTHOG_API_KEY is unset (PostHogService short-circuits), so * self-hosted installs are unaffected. */ -class SyncUserToPostHog implements ShouldQueue +class SyncUser implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; diff --git a/app/Jobs/PostHog/TrackBilling.php b/app/Jobs/PostHog/TrackBilling.php new file mode 100644 index 00000000..ca8475a7 --- /dev/null +++ b/app/Jobs/PostHog/TrackBilling.php @@ -0,0 +1,63 @@ + $payload + */ + public function __construct( + public string $accountId, + public string $event, + public array $payload, + ) { + $this->onQueue('posthog'); + } + + public function handle(PostHogService $postHog): void + { + $account = Account::with('plan')->find($this->accountId); + + if (! $account || ! $account->owner_id) { + return; + } + + $postHog->capture( + (string) $account->owner_id, + $this->event, + [ + 'stripe_status' => data_get($this->payload, 'data.object.status'), + 'plan' => $account->plan?->name, + 'plan_slug' => $account->plan?->slug, + ], + $account, + ); + + SyncUser::dispatch((string) $account->owner_id); + } +} diff --git a/app/Listeners/StripeEventListener.php b/app/Listeners/StripeEventListener.php index 38e2b5a7..20132b96 100644 --- a/app/Listeners/StripeEventListener.php +++ b/app/Listeners/StripeEventListener.php @@ -5,14 +5,19 @@ namespace App\Listeners; use App\Events\SubscriptionCreated; -use App\Jobs\SyncUserToPostHog; +use App\Jobs\PostHog\TrackBilling; use App\Models\Account; -use App\Services\PostHogService; use Illuminate\Support\Facades\Log; use Laravel\Cashier\Events\WebhookReceived; class StripeEventListener { + private const POSTHOG_EVENT_BY_TYPE = [ + 'customer.subscription.created' => 'subscription.created', + 'customer.subscription.updated' => 'subscription.updated', + 'customer.subscription.deleted' => 'subscription.cancelled', + ]; + public function handle(WebhookReceived $event): void { try { @@ -29,12 +34,13 @@ public function handle(WebhookReceived $event): void return; } - match ($type) { - 'customer.subscription.created' => $this->handleSubscriptionCreated($account, $event->payload), - 'customer.subscription.updated' => $this->handleSubscriptionUpdated($account, $event->payload), - 'customer.subscription.deleted' => $this->handleSubscriptionDeleted($account, $event->payload), - default => null, - }; + if ($type === 'customer.subscription.created') { + SubscriptionCreated::dispatch($account); + } + + if ($postHogEvent = self::POSTHOG_EVENT_BY_TYPE[$type] ?? null) { + TrackBilling::dispatch((string) $account->id, $postHogEvent, $event->payload); + } } catch (\Exception $e) { Log::error('Stripe webhook error: '.$e->getMessage(), [ 'exception' => $e, @@ -42,59 +48,4 @@ public function handle(WebhookReceived $event): void ]); } } - - /** - * @param array $payload - */ - protected function handleSubscriptionCreated(Account $account, array $payload): void - { - SubscriptionCreated::dispatch($account); - - $this->trackBilling($account, 'subscription.created', $payload); - } - - /** - * @param array $payload - */ - protected function handleSubscriptionUpdated(Account $account, array $payload): void - { - $this->trackBilling($account, 'subscription.updated', $payload); - } - - /** - * @param array $payload - */ - protected function handleSubscriptionDeleted(Account $account, array $payload): void - { - $this->trackBilling($account, 'subscription.cancelled', $payload); - } - - /** - * Capture the lifecycle event on the account owner's profile and trigger - * a fresh `SyncUserToPostHog` so the account group properties (plan, - * has_active_subscription, is_on_trial) reflect the new Stripe state. - * - * @param array $payload - */ - private function trackBilling(Account $account, string $event, array $payload): void - { - if (! $account->owner_id) { - return; - } - - $properties = [ - 'stripe_status' => data_get($payload, 'data.object.status'), - 'plan' => $account->plan?->name, - 'plan_slug' => $account->plan?->slug, - ]; - - app(PostHogService::class)->capture( - (string) $account->owner_id, - $event, - $properties, - $account, - ); - - SyncUserToPostHog::dispatch((string) $account->owner_id); - } } diff --git a/app/Services/PostHogService.php b/app/Services/PostHogService.php index 5c23b457..23e3214b 100644 --- a/app/Services/PostHogService.php +++ b/app/Services/PostHogService.php @@ -4,7 +4,7 @@ namespace App\Services; -use App\Jobs\SendPostHogEvent; +use App\Jobs\PostHog\SendEvent; use App\Models\Account; use Illuminate\Support\Facades\Log; @@ -75,7 +75,7 @@ public function groupIdentify(string $groupType, string $groupKey, array $proper private function dispatch(string $method, array $payload): void { try { - SendPostHogEvent::dispatch([ + SendEvent::dispatch([ ['method' => $method, 'payload' => $payload], ]); } catch (\Throwable $e) { diff --git a/resources/js/app.ts b/resources/js/app.ts index 231f3389..6b54db35 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -1,7 +1,6 @@ import '../css/app.css'; import { createInertiaApp, router } from '@inertiajs/vue3'; -import type { Page } from '@inertiajs/core'; import { configureEcho } from '@laravel/echo-vue'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import { i18nVue } from 'laravel-vue-i18n'; @@ -10,54 +9,15 @@ import { createApp, h } from 'vue'; import { initializeDataLayer } from './datalayer'; import dayjs from './dayjs'; -import posthog from './posthog'; +import { capturePageview, syncPostHogContext } from './posthog'; import type { Auth } from './types'; -interface Usage { - workspaceCount: number; - socialAccountCount: number; - memberCount: number; - pendingInviteCount: number; - postCount: number; - creditsUsed: number; -} - configureEcho({ broadcaster: 'reverb', }); const appName = import.meta.env.VITE_APP_NAME || 'TryPost.it'; -// Re-applies the PostHog account/workspace group context with fresh metrics. -// Called on every Inertia navigation so `usage` counts (workspaces, social -// accounts, posts, members) stay reactive — the backend already ships these -// counts on every Inertia request, so this leverages props that are already -// loaded without firing extra queries. -const syncPostHogContext = (page: Page): void => { - const auth = page.props.auth as Auth | undefined; - const usage = page.props.usage as Usage | null; - - if (!auth?.user) return; - - if (auth.account) { - posthog.group('account', auth.account.id, { - name: auth.account.name, - workspaces_count: usage?.workspaceCount, - social_accounts_count: usage?.socialAccountCount, - members_count: usage?.memberCount, - posts_count: usage?.postCount, - credits_used: usage?.creditsUsed, - }); - } - - if (auth.currentWorkspace) { - posthog.group('workspace', auth.currentWorkspace.id, { - name: auth.currentWorkspace.name, - account_id: auth.account?.id, - }); - } -}; - createInertiaApp({ title: (title) => (title ? `${title} - ${appName}` : appName), resolve: (name) => @@ -84,26 +44,16 @@ createInertiaApp({ props.initialPage.props.env as string, ); - if (auth?.user) { - posthog.identify(auth.user.id, { - $email: auth.user.email, - $name: auth.user.name, - }); - } - - // Initial group context + initial pageview. The backend mirrors this - // hierarchy in app/Jobs/SyncUserToPostHog.php so events emitted from - // the server land on the same person + group identifiers. + // Initial PostHog identify + dual-group context + first pageview. + // The same hooks fire on every Inertia navigation below so the + // account group counts stay reactive and workspace switches + // re-attach the right workspace group. syncPostHogContext(props.initialPage); - posthog.capture('$pageview', { $current_url: window.location.href }); + capturePageview(); router.on('navigate', (event) => { - // Re-sync group context on every navigation: refreshes the count - // metrics on the `account` group AND covers workspace switches - // (which update auth.currentWorkspace via Inertia's prop refresh - // without triggering setup() again). syncPostHogContext(event.detail.page); - posthog.capture('$pageview', { $current_url: window.location.href }); + capturePageview(); }); createApp({ render: () => h(App, props) }) diff --git a/resources/js/posthog.ts b/resources/js/posthog.ts index 54f53c7b..11e61d13 100644 --- a/resources/js/posthog.ts +++ b/resources/js/posthog.ts @@ -1,5 +1,8 @@ +import type { Page } from '@inertiajs/core'; import posthog from 'posthog-js'; +import type { Auth } from './types'; + const apiKey = import.meta.env.VITE_POSTHOG_API_KEY as string | undefined; const host = import.meta.env.VITE_POSTHOG_HOST as string | undefined; @@ -18,4 +21,68 @@ if (apiKey) { }); } +interface Usage { + workspaceCount: number; + socialAccountCount: number; + memberCount: number; + pendingInviteCount: number; + postCount: number; + creditsUsed: number; +} + +/** + * Identify the current person and refresh the dual group context (account + + * workspace) using the metrics already shipped by Inertia in shared props. + * + * Called once at boot and on every Inertia navigation so: + * - Counts on the `account` group (workspaces, social accounts, posts, + * members, credits) stay reactive without per-domain backend triggers. + * - Workspace switches re-attach the right `workspace` group (the + * navigation event carries the fresh `auth.currentWorkspace`). + * + * Hierarchy mirrors the backend `app/Jobs/SyncUserToPostHog.php`: + * person → User, group `account` → billing/plan parent, + * group `workspace` → collaboration child (carries `account_id`). + */ +export const syncPostHogContext = (page: Page): void => { + const auth = page.props.auth as Auth | undefined; + const usage = page.props.usage as Usage | null | undefined; + + if (!auth?.user) { + return; + } + + posthog.identify(auth.user.id, { + $email: auth.user.email, + $name: auth.user.name, + }); + + if (auth.account) { + posthog.group('account', auth.account.id, { + name: auth.account.name, + workspaces_count: usage?.workspaceCount, + social_accounts_count: usage?.socialAccountCount, + members_count: usage?.memberCount, + posts_count: usage?.postCount, + credits_used: usage?.creditsUsed, + }); + } + + if (auth.currentWorkspace) { + posthog.group('workspace', auth.currentWorkspace.id, { + name: auth.currentWorkspace.name, + account_id: auth.account?.id, + }); + } +}; + +/** + * Capture an Inertia-driven pageview. The base SDK is configured with + * `capture_pageview: false`, so this needs to be invoked explicitly on + * every navigation (and once at boot for the first page). + */ +export const capturePageview = (): void => { + posthog.capture('$pageview', { $current_url: window.location.href }); +}; + export default posthog; diff --git a/tests/Feature/Actions/User/CreateUserTest.php b/tests/Feature/Actions/User/CreateUserTest.php index 0f83bb98..eb46dee1 100644 --- a/tests/Feature/Actions/User/CreateUserTest.php +++ b/tests/Feature/Actions/User/CreateUserTest.php @@ -3,8 +3,10 @@ declare(strict_types=1); use App\Actions\User\CreateUser; +use App\Jobs\PostHog\SyncUser; use App\Models\Account; use App\Models\Workspace; +use Illuminate\Support\Facades\Bus; test('CreateUser creates user with account but no workspace and no current_workspace_id', function () { $user = CreateUser::execute([ @@ -42,3 +44,18 @@ expect($user->email_verified_at)->not->toBeNull(); expect(Workspace::count())->toBe(0); }); + +test('CreateUser dispatches SyncUser with the new user id', function () { + Bus::fake([SyncUser::class]); + + $user = CreateUser::execute([ + 'name' => 'Jane Doe', + 'email' => 'jane.posthog@example.com', + 'password' => 'secret123', + ]); + + Bus::assertDispatched( + SyncUser::class, + fn ($job) => $job->userId === (string) $user->id, + ); +}); diff --git a/tests/Feature/SendPostHogEventTest.php b/tests/Feature/Jobs/PostHog/SendEventTest.php similarity index 83% rename from tests/Feature/SendPostHogEventTest.php rename to tests/Feature/Jobs/PostHog/SendEventTest.php index ec0bf2b6..0c93aaa9 100644 --- a/tests/Feature/SendPostHogEventTest.php +++ b/tests/Feature/Jobs/PostHog/SendEventTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -use App\Jobs\SendPostHogEvent; +use App\Jobs\PostHog\SendEvent; test('job is queued on posthog queue', function () { - $job = new SendPostHogEvent([ + $job = new SendEvent([ ['method' => 'capture', 'payload' => ['distinctId' => 'user-1', 'event' => 'test']], ]); @@ -15,7 +15,7 @@ test('job skips execution when api key is missing', function () { config(['services.posthog.api_key' => null]); - $job = new SendPostHogEvent([ + $job = new SendEvent([ ['method' => 'capture', 'payload' => ['distinctId' => 'user-1', 'event' => 'test']], ]); @@ -26,7 +26,7 @@ }); test('job has correct retry and timeout settings', function () { - $job = new SendPostHogEvent([]); + $job = new SendEvent([]); expect($job->tries)->toBe(3); expect($job->timeout)->toBe(15); diff --git a/tests/Feature/Jobs/PostHog/SyncUserTest.php b/tests/Feature/Jobs/PostHog/SyncUserTest.php new file mode 100644 index 00000000..c4c2620e --- /dev/null +++ b/tests/Feature/Jobs/PostHog/SyncUserTest.php @@ -0,0 +1,139 @@ + 'phc_test_key']); + + $this->account = Account::factory()->create([ + 'plan_id' => Plan::query()->where('slug', 'starter')->first()?->id, + ]); + $this->user = User::factory()->create(['account_id' => $this->account->id]); + $this->account->update(['owner_id' => $this->user->id]); +}); + +test('handle is a no-op when api key is unset', function () { + config(['services.posthog.api_key' => null]); + Queue::fake(); + + (new SyncUser((string) $this->user->id))->handle(app(PostHogService::class)); + + Queue::assertNothingPushed(); +}); + +test('handle returns silently when user does not exist', function () { + Queue::fake(); + + (new SyncUser((string) Str::uuid()))->handle(app(PostHogService::class)); + + Queue::assertNothingPushed(); +}); + +test('handle identifies the user with email, name and signed_up_at', function () { + Queue::fake(); + + (new SyncUser((string) $this->user->id))->handle(app(PostHogService::class)); + + Queue::assertPushed(SendEvent::class, function ($job) { + $call = $job->calls[0]; + + return $call['method'] === 'identify' + && $call['payload']['distinctId'] === (string) $this->user->id + && $call['payload']['properties']['email'] === $this->user->email + && $call['payload']['properties']['name'] === $this->user->name + && isset($call['payload']['properties']['$set_once']['signed_up_at']); + }); +}); + +test('handle group-identifies the account with usage metrics', function () { + $workspace = Workspace::factory()->create([ + 'account_id' => $this->account->id, + 'user_id' => $this->user->id, + ]); + SocialAccount::factory()->count(2)->create(['workspace_id' => $workspace->id]); + Post::factory()->count(3)->create([ + 'workspace_id' => $workspace->id, + 'user_id' => $this->user->id, + ]); + + Queue::fake(); + + (new SyncUser((string) $this->user->id))->handle(app(PostHogService::class)); + + Queue::assertPushed(SendEvent::class, function ($job) { + $accountCall = collect($job->calls) + ->merge(collect(Queue::pushed(SendEvent::class))->flatMap(fn ($j) => $j->calls)) + ->first(fn ($c) => $c['method'] === 'groupIdentify' && $c['payload']['groupType'] === 'account'); + + if (! $accountCall) { + return false; + } + + $props = $accountCall['payload']['properties']; + + return $accountCall['payload']['groupKey'] === (string) $this->account->id + && $props['workspaces_count'] === 1 + && $props['social_accounts_count'] === 2 + && $props['posts_count'] === 3 + && $props['members_count'] === 1 + && array_key_exists('plan', $props) + && array_key_exists('has_active_subscription', $props) + && array_key_exists('is_on_trial', $props); + }); +}); + +test('handle group-identifies the current workspace when set', function () { + $workspace = Workspace::factory()->create([ + 'account_id' => $this->account->id, + 'user_id' => $this->user->id, + ]); + SocialAccount::factory()->create(['workspace_id' => $workspace->id]); + $this->user->update(['current_workspace_id' => $workspace->id]); + + Queue::fake(); + + (new SyncUser((string) $this->user->id))->handle(app(PostHogService::class)); + + $allCalls = collect(Queue::pushed(SendEvent::class))->flatMap(fn ($j) => $j->calls); + $workspaceCall = $allCalls->first( + fn ($c) => $c['method'] === 'groupIdentify' && $c['payload']['groupType'] === 'workspace', + ); + + expect($workspaceCall)->not->toBeNull(); + expect($workspaceCall['payload']['groupKey'])->toBe((string) $workspace->id); + expect($workspaceCall['payload']['properties']['account_id'])->toBe((string) $this->account->id); + expect($workspaceCall['payload']['properties']['social_accounts_count'])->toBe(1); +}); + +test('handle skips workspace group identify when user has no current workspace', function () { + $this->user->update(['current_workspace_id' => null]); + + Queue::fake(); + + (new SyncUser((string) $this->user->id))->handle(app(PostHogService::class)); + + $allCalls = collect(Queue::pushed(SendEvent::class))->flatMap(fn ($j) => $j->calls); + $workspaceCall = $allCalls->first( + fn ($c) => $c['method'] === 'groupIdentify' && $c['payload']['groupType'] === 'workspace', + ); + + expect($workspaceCall)->toBeNull(); +}); + +test('job is queued on the posthog connection queue', function () { + $job = new SyncUser((string) $this->user->id); + + expect($job->queue)->toBe('posthog'); +}); diff --git a/tests/Feature/Jobs/PostHog/TrackBillingTest.php b/tests/Feature/Jobs/PostHog/TrackBillingTest.php new file mode 100644 index 00000000..eb47e9f8 --- /dev/null +++ b/tests/Feature/Jobs/PostHog/TrackBillingTest.php @@ -0,0 +1,98 @@ + 'phc_test_key']); + + $this->account = Account::factory()->create([ + 'plan_id' => Plan::query()->where('slug', 'starter')->first()?->id, + ]); + $this->user = User::factory()->create(['account_id' => $this->account->id]); + $this->account->update(['owner_id' => $this->user->id]); + + $this->payload = [ + 'type' => 'customer.subscription.updated', + 'data' => ['object' => ['customer' => 'cus_test', 'status' => 'active']], + ]; +}); + +test('job is queued on the posthog queue', function () { + $job = new TrackBilling((string) $this->account->id, 'subscription.updated', $this->payload); + + expect($job->queue)->toBe('posthog'); +}); + +test('handle captures event on the owner profile with account group attached', function () { + Queue::fake(); + + (new TrackBilling((string) $this->account->id, 'subscription.updated', $this->payload)) + ->handle(app(PostHogService::class)); + + Queue::assertPushed(SendEvent::class, function ($job) { + $call = $job->calls[0]; + + return $call['method'] === 'capture' + && $call['payload']['event'] === 'subscription.updated' + && $call['payload']['distinctId'] === (string) $this->user->id + && $call['payload']['properties']['$groups']['account'] === (string) $this->account->id + && $call['payload']['properties']['stripe_status'] === 'active'; + }); +}); + +test('handle dispatches SyncUser for the account owner', function () { + Bus::fake([SyncUser::class]); + + (new TrackBilling((string) $this->account->id, 'subscription.cancelled', $this->payload)) + ->handle(app(PostHogService::class)); + + Bus::assertDispatched( + SyncUser::class, + fn ($job) => $job->userId === (string) $this->user->id, + ); +}); + +test('handle returns silently when account does not exist', function () { + Queue::fake(); + Bus::fake([SyncUser::class]); + + (new TrackBilling('00000000-0000-0000-0000-000000000000', 'subscription.created', $this->payload)) + ->handle(app(PostHogService::class)); + + Queue::assertNothingPushed(); + Bus::assertNotDispatched(SyncUser::class); +}); + +test('handle returns silently when account has no owner', function () { + $this->account->update(['owner_id' => null]); + Queue::fake(); + Bus::fake([SyncUser::class]); + + (new TrackBilling((string) $this->account->id, 'subscription.created', $this->payload)) + ->handle(app(PostHogService::class)); + + Queue::assertNothingPushed(); + Bus::assertNotDispatched(SyncUser::class); +}); + +test('handle does not push a PostHog network call when api key is unset', function () { + config(['services.posthog.api_key' => null]); + Queue::fake(); + + (new TrackBilling((string) $this->account->id, 'subscription.created', $this->payload)) + ->handle(app(PostHogService::class)); + + // SyncUser can still be queued but it would itself no-op when handled. + // The contract of this job is: no PostHog network call without a key. + Queue::assertNotPushed(SendEvent::class); +}); diff --git a/tests/Feature/Listeners/StripeEventListenerTest.php b/tests/Feature/Listeners/StripeEventListenerTest.php index 362871ed..6a785bb3 100644 --- a/tests/Feature/Listeners/StripeEventListenerTest.php +++ b/tests/Feature/Listeners/StripeEventListenerTest.php @@ -3,9 +3,11 @@ declare(strict_types=1); use App\Events\SubscriptionCreated; +use App\Jobs\PostHog\TrackBilling; use App\Listeners\StripeEventListener; use App\Models\Account; use App\Models\User; +use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Log; use Laravel\Cashier\Events\WebhookReceived; @@ -126,7 +128,6 @@ ->once() ->withArgs(fn ($msg) => str_contains($msg, 'Stripe webhook error')); - // Force an exception by making Event::dispatch throw Event::listen(SubscriptionCreated::class, fn () => throw new Exception('Simulated error')); $this->listener->handle(new WebhookReceived([ @@ -142,3 +143,61 @@ expect(true)->toBeTrue(); }); + +// ======================================== +// PostHog tracking — listener delegates to TrackBilling +// ======================================== + +test('subscription created dispatches TrackBilling with subscription.created event', function () { + Bus::fake([TrackBilling::class]); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.created', + 'data' => ['object' => ['customer' => 'cus_test123', 'id' => 'sub_123', 'status' => 'trialing']], + ])); + + Bus::assertDispatched( + TrackBilling::class, + fn ($job) => $job->accountId === (string) $this->account->id + && $job->event === 'subscription.created', + ); +}); + +test('subscription updated dispatches TrackBilling with subscription.updated event', function () { + Bus::fake([TrackBilling::class]); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.updated', + 'data' => ['object' => ['customer' => 'cus_test123', 'status' => 'active']], + ])); + + Bus::assertDispatched( + TrackBilling::class, + fn ($job) => $job->event === 'subscription.updated', + ); +}); + +test('subscription deleted dispatches TrackBilling with subscription.cancelled event', function () { + Bus::fake([TrackBilling::class]); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.deleted', + 'data' => ['object' => ['customer' => 'cus_test123', 'status' => 'canceled']], + ])); + + Bus::assertDispatched( + TrackBilling::class, + fn ($job) => $job->event === 'subscription.cancelled', + ); +}); + +test('unknown event types do not dispatch TrackBilling', function () { + Bus::fake([TrackBilling::class]); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'invoice.payment_succeeded', + 'data' => ['object' => ['customer' => 'cus_test123']], + ])); + + Bus::assertNotDispatched(TrackBilling::class); +}); diff --git a/tests/Unit/PostHogServiceTest.php b/tests/Unit/PostHogServiceTest.php index 34f0938d..9bf0425a 100644 --- a/tests/Unit/PostHogServiceTest.php +++ b/tests/Unit/PostHogServiceTest.php @@ -2,10 +2,15 @@ declare(strict_types=1); -use App\Jobs\SendPostHogEvent; +use App\Jobs\PostHog\SendEvent; +use App\Models\Account; +use App\Models\Plan; use App\Services\PostHogService; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Queue; +uses(RefreshDatabase::class); + test('capture dispatches job when api key is configured', function () { Queue::fake(); config(['services.posthog.api_key' => 'phc_test_key']); @@ -13,7 +18,7 @@ $service = new PostHogService; $service->capture('user-123', 'test_event', ['foo' => 'bar']); - Queue::assertPushed(SendPostHogEvent::class, function ($job) { + Queue::assertPushed(SendEvent::class, function ($job) { return $job->calls[0]['method'] === 'capture' && $job->calls[0]['payload']['event'] === 'test_event' && $job->calls[0]['payload']['distinctId'] === 'user-123'; @@ -37,7 +42,7 @@ $service = new PostHogService; $service->identify('user-123', ['$email' => 'test@example.com']); - Queue::assertPushed(SendPostHogEvent::class, function ($job) { + Queue::assertPushed(SendEvent::class, function ($job) { return $job->calls[0]['method'] === 'identify' && $job->calls[0]['payload']['distinctId'] === 'user-123'; }); @@ -60,7 +65,7 @@ $service = new PostHogService; $service->groupIdentify('workspace', 'ws-123', ['name' => 'Test Workspace']); - Queue::assertPushed(SendPostHogEvent::class, function ($job) { + Queue::assertPushed(SendEvent::class, function ($job) { return $job->calls[0]['method'] === 'groupIdentify' && $job->calls[0]['payload']['groupType'] === 'workspace' && $job->calls[0]['payload']['groupKey'] === 'ws-123'; @@ -76,3 +81,40 @@ Queue::assertNothingPushed(); }); + +test('capture auto-attaches account groups when account is supplied', function () { + Queue::fake(); + config(['services.posthog.api_key' => 'phc_test_key']); + + $plan = Plan::query()->where('slug', 'starter')->first(); + $account = Account::factory()->create(['plan_id' => $plan?->id]); + + $service = new PostHogService; + $service->capture('user-123', 'subscription.created', ['stripe_status' => 'active'], $account); + + Queue::assertPushed(SendEvent::class, function ($job) use ($account, $plan) { + $payload = $job->calls[0]['payload']; + + return $payload['event'] === 'subscription.created' + && $payload['properties']['$groups']['account'] === (string) $account->id + && $payload['properties']['account_id'] === (string) $account->id + && $payload['properties']['plan'] === $plan?->name + && $payload['properties']['stripe_status'] === 'active'; + }); +}); + +test('capture without account does not attach group properties', function () { + Queue::fake(); + config(['services.posthog.api_key' => 'phc_test_key']); + + $service = new PostHogService; + $service->capture('user-123', 'page.viewed', ['url' => '/dashboard']); + + Queue::assertPushed(SendEvent::class, function ($job) { + $properties = $job->calls[0]['payload']['properties']; + + return ! array_key_exists('$groups', $properties) + && ! array_key_exists('account_id', $properties) + && ! array_key_exists('plan', $properties); + }); +}); From 9bc9e1f93c76cda7f0a9b3f6754e7b2e89c4cc52 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 7 May 2026 10:10:23 -0300 Subject: [PATCH 03/15] feat: wire StripeEventListener domain logic for plan swap & cancel The previous version of `handleSubscriptionUpdated` and `handleSubscriptionDeleted` were no-ops, so a plan swap or cancellation on Stripe never reflected in `accounts.plan_id`. Mirrors sendkit's listener structure and adds full coverage. Listener: - `handleSubscriptionCreated` / `handleSubscriptionUpdated` capture the previous plan name, resolve the new plan from the subscription items' price ids, and update `account.plan_id` if it changed. Pennant feature caches are forgotten automatically by `Account::booted()`. - `handleSubscriptionDeleted` clears `account.plan_id` so the UI and authorization checks reflect "no plan" instead of keeping the previous one attached. Idempotent. - Helpers extracted: `resolvePlanFromSubscriptionItems(payload, account)` (pure resolver, logs a warning when no plan matches) and `trackPlanChange(account, event, previousPlan, payload)` (delegates to the queued `TrackBilling` job). - Restored the `match($type) => handler...` shape with explicit protected handler methods for each subscription type. TrackBilling: - Adds optional `?string $previousPlan` constructor param. Forwarded as `previous_plan` event property so PostHog funnels can tell upgrade from downgrade from cancellation. Tests: - New: subscription updated/created sync `plan_id` from price ids (monthly + yearly), idempotent when price already matches, ignored when price ids are unknown, deletion clears plan_id, deletion idempotent when already null. Tests override the seeded plans' Stripe price ids inline so they don't depend on `.env.testing` having `STRIPE_*_MONTHLY/YEARLY` set. - New: previousPlan is forwarded to `TrackBilling` for updated and deleted, and is null for first-activation. - TrackBilling test: covers the previous_plan property in capture payload (both supplied and default-null cases). Suite: 1438 passed (+11). Plus a tiny cleanup: replaced the `'PostHog\\SendEvent: ...'` namespace-look-alike in the SendEvent log warning with the cleaner `'PostHog SendEvent: ...'`. --- app/Jobs/PostHog/SendEvent.php | 2 +- app/Jobs/PostHog/TrackBilling.php | 2 + app/Listeners/StripeEventListener.php | 123 +++++++++-- .../Feature/Jobs/PostHog/TrackBillingTest.php | 14 +- .../Listeners/StripeEventListenerTest.php | 192 ++++++++++++++++++ 5 files changed, 318 insertions(+), 15 deletions(-) diff --git a/app/Jobs/PostHog/SendEvent.php b/app/Jobs/PostHog/SendEvent.php index 0a2ef992..75f679e6 100644 --- a/app/Jobs/PostHog/SendEvent.php +++ b/app/Jobs/PostHog/SendEvent.php @@ -50,7 +50,7 @@ public function handle(): void 'capture' => PostHog::capture(data_get($call, 'payload')), 'identify' => PostHog::identify(data_get($call, 'payload')), 'groupIdentify' => PostHog::groupIdentify(data_get($call, 'payload')), - default => Log::warning('PostHog\\SendEvent: unknown method', ['method' => data_get($call, 'method')]), + default => Log::warning('PostHog SendEvent: unknown method', ['method' => data_get($call, 'method')]), }; } diff --git a/app/Jobs/PostHog/TrackBilling.php b/app/Jobs/PostHog/TrackBilling.php index ca8475a7..aa4f9ce2 100644 --- a/app/Jobs/PostHog/TrackBilling.php +++ b/app/Jobs/PostHog/TrackBilling.php @@ -35,6 +35,7 @@ public function __construct( public string $accountId, public string $event, public array $payload, + public ?string $previousPlan = null, ) { $this->onQueue('posthog'); } @@ -54,6 +55,7 @@ public function handle(PostHogService $postHog): void 'stripe_status' => data_get($this->payload, 'data.object.status'), 'plan' => $account->plan?->name, 'plan_slug' => $account->plan?->slug, + 'previous_plan' => $this->previousPlan, ], $account, ); diff --git a/app/Listeners/StripeEventListener.php b/app/Listeners/StripeEventListener.php index 20132b96..6fc0f039 100644 --- a/app/Listeners/StripeEventListener.php +++ b/app/Listeners/StripeEventListener.php @@ -7,17 +7,12 @@ use App\Events\SubscriptionCreated; use App\Jobs\PostHog\TrackBilling; use App\Models\Account; +use App\Models\Plan; use Illuminate\Support\Facades\Log; use Laravel\Cashier\Events\WebhookReceived; class StripeEventListener { - private const POSTHOG_EVENT_BY_TYPE = [ - 'customer.subscription.created' => 'subscription.created', - 'customer.subscription.updated' => 'subscription.updated', - 'customer.subscription.deleted' => 'subscription.cancelled', - ]; - public function handle(WebhookReceived $event): void { try { @@ -34,13 +29,12 @@ public function handle(WebhookReceived $event): void return; } - if ($type === 'customer.subscription.created') { - SubscriptionCreated::dispatch($account); - } - - if ($postHogEvent = self::POSTHOG_EVENT_BY_TYPE[$type] ?? null) { - TrackBilling::dispatch((string) $account->id, $postHogEvent, $event->payload); - } + match ($type) { + 'customer.subscription.created' => $this->handleSubscriptionCreated($account, $event->payload), + 'customer.subscription.updated' => $this->handleSubscriptionUpdated($account, $event->payload), + 'customer.subscription.deleted' => $this->handleSubscriptionDeleted($account, $event->payload), + default => null, + }; } catch (\Exception $e) { Log::error('Stripe webhook error: '.$e->getMessage(), [ 'exception' => $e, @@ -48,4 +42,107 @@ public function handle(WebhookReceived $event): void ]); } } + + /** + * @param array $payload + */ + protected function handleSubscriptionCreated(Account $account, array $payload): void + { + $previousPlan = $account->plan?->name; + $plan = $this->resolvePlanFromSubscriptionItems($payload, $account); + + if ($plan && $account->plan_id !== $plan->id) { + $account->update(['plan_id' => $plan->id]); + } + + SubscriptionCreated::dispatch($account); + + $this->trackPlanChange($account, 'subscription.created', $previousPlan, $payload); + } + + /** + * Plan changes coming from Stripe (billing portal, dashboard, or our own + * `BillingController@swap`) re-arrive here. Re-resolving the plan from + * the price ids guarantees the local state matches Stripe even when a + * change happens out-of-band. Pennant feature caches are forgotten + * automatically via Account::booted() when `plan_id` changes. + * + * @param array $payload + */ + protected function handleSubscriptionUpdated(Account $account, array $payload): void + { + $previousPlan = $account->plan?->name; + $plan = $this->resolvePlanFromSubscriptionItems($payload, $account); + + if ($plan && $account->plan_id !== $plan->id) { + $account->update(['plan_id' => $plan->id]); + } + + $this->trackPlanChange($account, 'subscription.updated', $previousPlan, $payload); + } + + /** + * Subscription fully terminated (period ended after cancellation, or + * cancelled immediately). Clear the local plan so the UI and + * authorisation checks (`Account::hasActiveSubscription`) reflect + * "no plan" instead of keeping the previous one attached. + * + * @param array $payload + */ + protected function handleSubscriptionDeleted(Account $account, array $payload): void + { + $previousPlan = $account->plan?->name; + + if ($account->plan_id !== null) { + $account->update(['plan_id' => null]); + } + + $this->trackPlanChange($account, 'subscription.cancelled', $previousPlan, $payload); + } + + /** + * Resolve the matching `Plan` for a Stripe subscription payload by + * looking up the items' price ids against the local plans table. + * Logs a warning when no match is found so price/plan drift between + * Stripe and the local DB is visible. + * + * @param array $payload + */ + private function resolvePlanFromSubscriptionItems(array $payload, Account $account): ?Plan + { + $priceIds = collect(data_get($payload, 'data.object.items.data', [])) + ->pluck('price.id') + ->filter() + ->all(); + + if (empty($priceIds)) { + return null; + } + + $plan = Plan::query() + ->where(function ($query) use ($priceIds): void { + $query->whereIn('stripe_monthly_price_id', $priceIds) + ->orWhereIn('stripe_yearly_price_id', $priceIds); + }) + ->first(); + + if (! $plan) { + Log::warning('Stripe webhook: no matching plan found in subscription items', [ + 'account_id' => $account->id, + 'price_ids' => $priceIds, + ]); + } + + return $plan; + } + + /** + * Hand the lifecycle event off to the queue so the listener stays fast. + * + * @param array $payload + */ + private function trackPlanChange(Account $account, string $event, ?string $previousPlan, array $payload): void + { + TrackBilling::dispatch((string) $account->id, $event, $payload, $previousPlan); + } } diff --git a/tests/Feature/Jobs/PostHog/TrackBillingTest.php b/tests/Feature/Jobs/PostHog/TrackBillingTest.php index eb47e9f8..974ad67b 100644 --- a/tests/Feature/Jobs/PostHog/TrackBillingTest.php +++ b/tests/Feature/Jobs/PostHog/TrackBillingTest.php @@ -46,7 +46,19 @@ && $call['payload']['event'] === 'subscription.updated' && $call['payload']['distinctId'] === (string) $this->user->id && $call['payload']['properties']['$groups']['account'] === (string) $this->account->id - && $call['payload']['properties']['stripe_status'] === 'active'; + && $call['payload']['properties']['stripe_status'] === 'active' + && array_key_exists('previous_plan', $call['payload']['properties']); + }); +}); + +test('handle forwards previousPlan as a property when supplied', function () { + Queue::fake(); + + (new TrackBilling((string) $this->account->id, 'subscription.updated', $this->payload, 'Starter')) + ->handle(app(PostHogService::class)); + + Queue::assertPushed(SendEvent::class, function ($job) { + return $job->calls[0]['payload']['properties']['previous_plan'] === 'Starter'; }); }); diff --git a/tests/Feature/Listeners/StripeEventListenerTest.php b/tests/Feature/Listeners/StripeEventListenerTest.php index 6a785bb3..67c61705 100644 --- a/tests/Feature/Listeners/StripeEventListenerTest.php +++ b/tests/Feature/Listeners/StripeEventListenerTest.php @@ -6,6 +6,7 @@ use App\Jobs\PostHog\TrackBilling; use App\Listeners\StripeEventListener; use App\Models\Account; +use App\Models\Plan; use App\Models\User; use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Event; @@ -201,3 +202,194 @@ Bus::assertNotDispatched(TrackBilling::class); }); + +// ======================================== +// Plan sync — domain logic +// ======================================== +// +// Tests below override the seeded plans' Stripe price ids with deterministic +// values so the assertions don't depend on `.env.testing` having +// `STRIPE_*_MONTHLY/YEARLY` set. + +beforeEach(function () { + Plan::query()->where('slug', 'starter')->update([ + 'stripe_monthly_price_id' => 'price_starter_monthly', + 'stripe_yearly_price_id' => 'price_starter_yearly', + ]); + Plan::query()->where('slug', 'pro')->update([ + 'stripe_monthly_price_id' => 'price_pro_monthly', + 'stripe_yearly_price_id' => 'price_pro_yearly', + ]); +}); + +test('subscription updated swaps account plan_id when price matches a different plan', function () { + $starter = Plan::query()->where('slug', 'starter')->firstOrFail(); + $pro = Plan::query()->where('slug', 'pro')->firstOrFail(); + + $this->account->update(['plan_id' => $starter->id]); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.updated', + 'data' => ['object' => [ + 'customer' => 'cus_test123', + 'items' => ['data' => [ + ['price' => ['id' => 'price_pro_monthly']], + ]], + ]], + ])); + + expect($this->account->fresh()->plan_id)->toBe($pro->id); +}); + +test('subscription updated leaves plan_id alone when price already matches current plan', function () { + $starter = Plan::query()->where('slug', 'starter')->firstOrFail(); + $this->account->update(['plan_id' => $starter->id]); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.updated', + 'data' => ['object' => [ + 'customer' => 'cus_test123', + 'items' => ['data' => [ + ['price' => ['id' => 'price_starter_monthly']], + ]], + ]], + ])); + + expect($this->account->fresh()->plan_id)->toBe($starter->id); +}); + +test('subscription updated ignores unknown price ids without erroring', function () { + $starter = Plan::query()->where('slug', 'starter')->firstOrFail(); + $this->account->update(['plan_id' => $starter->id]); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.updated', + 'data' => ['object' => [ + 'customer' => 'cus_test123', + 'items' => ['data' => [ + ['price' => ['id' => 'price_unknown_xyz']], + ]], + ]], + ])); + + expect($this->account->fresh()->plan_id)->toBe($starter->id); +}); + +test('subscription updated matches yearly price ids too', function () { + $starter = Plan::query()->where('slug', 'starter')->firstOrFail(); + $pro = Plan::query()->where('slug', 'pro')->firstOrFail(); + + $this->account->update(['plan_id' => $starter->id]); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.updated', + 'data' => ['object' => [ + 'customer' => 'cus_test123', + 'items' => ['data' => [ + ['price' => ['id' => 'price_pro_yearly']], + ]], + ]], + ])); + + expect($this->account->fresh()->plan_id)->toBe($pro->id); +}); + +test('subscription created syncs plan from price ids on first activation', function () { + $pro = Plan::query()->where('slug', 'pro')->firstOrFail(); + $this->account->update(['plan_id' => null]); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.created', + 'data' => ['object' => [ + 'customer' => 'cus_test123', + 'items' => ['data' => [ + ['price' => ['id' => 'price_pro_monthly']], + ]], + ]], + ])); + + expect($this->account->fresh()->plan_id)->toBe($pro->id); +}); + +test('subscription deleted clears the account plan_id', function () { + $starter = Plan::query()->where('slug', 'starter')->firstOrFail(); + $this->account->update(['plan_id' => $starter->id]); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.deleted', + 'data' => ['object' => ['customer' => 'cus_test123', 'status' => 'canceled']], + ])); + + expect($this->account->fresh()->plan_id)->toBeNull(); +}); + +test('subscription deleted is idempotent when plan_id is already null', function () { + $this->account->update(['plan_id' => null]); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.deleted', + 'data' => ['object' => ['customer' => 'cus_test123']], + ])); + + expect($this->account->fresh()->plan_id)->toBeNull(); +}); + +// ======================================== +// previousPlan propagation +// ======================================== + +test('subscription updated forwards the previous plan name to TrackBilling', function () { + $starter = Plan::query()->where('slug', 'starter')->firstOrFail(); + $this->account->update(['plan_id' => $starter->id]); + + Bus::fake([TrackBilling::class]); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.updated', + 'data' => ['object' => [ + 'customer' => 'cus_test123', + 'items' => ['data' => [['price' => ['id' => 'price_pro_monthly']]]], + ]], + ])); + + Bus::assertDispatched( + TrackBilling::class, + fn ($job) => $job->event === 'subscription.updated' && $job->previousPlan === $starter->name, + ); +}); + +test('subscription deleted forwards the previous plan name to TrackBilling', function () { + $starter = Plan::query()->where('slug', 'starter')->firstOrFail(); + $this->account->update(['plan_id' => $starter->id]); + + Bus::fake([TrackBilling::class]); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.deleted', + 'data' => ['object' => ['customer' => 'cus_test123']], + ])); + + Bus::assertDispatched( + TrackBilling::class, + fn ($job) => $job->event === 'subscription.cancelled' && $job->previousPlan === $starter->name, + ); +}); + +test('subscription created forwards a null previous plan when account had none', function () { + $this->account->update(['plan_id' => null]); + + Bus::fake([TrackBilling::class]); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.created', + 'data' => ['object' => [ + 'customer' => 'cus_test123', + 'items' => ['data' => [['price' => ['id' => 'price_starter_monthly']]]], + ]], + ])); + + Bus::assertDispatched( + TrackBilling::class, + fn ($job) => $job->event === 'subscription.created' && $job->previousPlan === null, + ); +}); From 1df59f09b46038afe9424b665cb44bb67c2f5ec6 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 7 May 2026 10:41:37 -0300 Subject: [PATCH 04/15] perf: cache account post count for 5 minutes in HasUsage Posts have no plan-quota gating, so a brief staleness on the count is acceptable. Avoids the heavy aggregate query on every authenticated request. Empty-account case skips cache writes entirely. --- app/Models/Traits/HasUsage.php | 30 ++++++++++++++++-- tests/Feature/Models/HasUsageTraitTest.php | 36 ++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/app/Models/Traits/HasUsage.php b/app/Models/Traits/HasUsage.php index b974e855..e0c3a07e 100644 --- a/app/Models/Traits/HasUsage.php +++ b/app/Models/Traits/HasUsage.php @@ -10,6 +10,8 @@ use App\Features\WorkspaceLimit; use App\Models\AiUsageLog; use App\Models\Invite; +use App\Models\Post; +use Illuminate\Support\Facades\Cache; use Laravel\Pennant\Feature; /** @@ -21,13 +23,21 @@ */ trait HasUsage { + /** + * Cache TTL for the per-account post count. Posts are unbounded by plan + * limits and not used for any quota gating, so a few minutes of staleness + * is acceptable in exchange for skipping a potentially heavy aggregate + * query on every authenticated request. + */ + private const POST_COUNT_CACHE_TTL = 300; + /** * @return array{workspaceCount: int, socialAccountCount: int, memberCount: int, pendingInviteCount: int, postCount: int, creditsUsed: int} */ public function usage(): array { $workspaces = $this->workspaces() - ->withCount(['socialAccounts', 'posts']) + ->withCount('socialAccounts') ->get(); return [ @@ -37,7 +47,7 @@ public function usage(): array 'pendingInviteCount' => Invite::where('account_id', $this->id) ->whereNull('accepted_at') ->count(), - 'postCount' => (int) $workspaces->sum('posts_count'), + 'postCount' => $this->cachedPostCount($workspaces->pluck('id')->all()), 'creditsUsed' => AiUsageLog::monthlyCredits($this->id), ]; } @@ -54,4 +64,20 @@ public function featureLimits(): array 'monthlyCreditsLimit' => Feature::for($this)->value(MonthlyCreditsLimit::class), ]; } + + /** + * @param array $workspaceIds + */ + private function cachedPostCount(array $workspaceIds): int + { + if (empty($workspaceIds)) { + return 0; + } + + return Cache::remember( + "account:{$this->id}:posts_count", + self::POST_COUNT_CACHE_TTL, + fn () => Post::whereIn('workspace_id', $workspaceIds)->count(), + ); + } } diff --git a/tests/Feature/Models/HasUsageTraitTest.php b/tests/Feature/Models/HasUsageTraitTest.php index e26d0d57..e19eea15 100644 --- a/tests/Feature/Models/HasUsageTraitTest.php +++ b/tests/Feature/Models/HasUsageTraitTest.php @@ -5,9 +5,11 @@ use App\Models\Account; use App\Models\Invite; use App\Models\Plan; +use App\Models\Post; use App\Models\SocialAccount; use App\Models\User; use App\Models\Workspace; +use Illuminate\Support\Facades\Cache; beforeEach(function () { $this->account = Account::factory()->create(); @@ -68,3 +70,37 @@ expect($this->account->usage()['pendingInviteCount'])->toBe(1); }); + +test('postCount is cached and survives new posts within the TTL', function () { + $workspace = Workspace::factory()->create([ + 'account_id' => $this->account->id, + 'user_id' => $this->owner->id, + ]); + + Post::factory()->count(2)->create([ + 'workspace_id' => $workspace->id, + 'user_id' => $this->owner->id, + ]); + + // First call primes the cache. + expect($this->account->usage()['postCount'])->toBe(2); + + // A new post is created mid-window. The cached value should win. + Post::factory()->create([ + 'workspace_id' => $workspace->id, + 'user_id' => $this->owner->id, + ]); + + expect($this->account->usage()['postCount'])->toBe(2); + + // Forgetting the cache key returns the fresh count. + Cache::forget("account:{$this->account->id}:posts_count"); + expect($this->account->usage()['postCount'])->toBe(3); +}); + +test('postCount returns zero without querying when account has no workspaces', function () { + expect($this->account->usage()['postCount'])->toBe(0); + + // No cache entry should be written for the empty case. + expect(Cache::has("account:{$this->account->id}:posts_count"))->toBeFalse(); +}); From b91bad7e6fb727698008a4d37bac6d04de555e00 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 7 May 2026 10:41:53 -0300 Subject: [PATCH 05/15] refactor: PostHog review polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New BillingEvent enum replaces 'subscription.{created,updated,cancelled}' strings across StripeEventListener, TrackBilling and tests. - SendEvent now takes (method, payload) directly instead of an array of single-call shapes — overhead with no batching benefit. - PostHogService consolidates the 3 api-key short-circuits into shouldSend(). - SyncUser eager-loads currentWorkspace.withCount('socialAccounts') and drops the redundant posts_count from the workspace group identify. - Frontend Usage interface centralised in resources/js/types — was duplicated in posthog.ts and useFeatureAccess.ts. - posthog.init moved out of module-import side-effect into initializePostHog() called explicitly from app.ts. - SyncUserTest cleans up the convoluted assertion that merged $job->calls with Queue::pushed(). - Drop tests/Feature/StripeEventListenerTest.php (orphan, fully covered by tests/Feature/Listeners/StripeEventListenerTest.php). - Revert .github/FUNDING.yml to match origin/main. --- .github/FUNDING.yml | 2 +- app/Enums/PostHog/BillingEvent.php | 12 ++++ app/Jobs/PostHog/SendEvent.php | 28 ++++---- app/Jobs/PostHog/SyncUser.php | 8 ++- app/Jobs/PostHog/TrackBilling.php | 5 +- app/Listeners/StripeEventListener.php | 19 +++-- app/Services/PostHogService.php | 15 ++-- resources/js/app.ts | 3 +- resources/js/composables/useFeatureAccess.ts | 11 +-- resources/js/posthog.ts | 19 ++--- resources/js/types/index.d.ts | 9 +++ tests/Feature/Jobs/PostHog/SendEventTest.php | 10 +-- tests/Feature/Jobs/PostHog/SyncUserTest.php | 47 +++++-------- .../Feature/Jobs/PostHog/TrackBillingTest.php | 31 ++++---- .../Listeners/StripeEventListenerTest.php | 13 ++-- tests/Feature/StripeEventListenerTest.php | 70 ------------------- tests/Unit/PostHogServiceTest.php | 20 +++--- 17 files changed, 124 insertions(+), 198 deletions(-) create mode 100644 app/Enums/PostHog/BillingEvent.php delete mode 100644 tests/Feature/StripeEventListenerTest.php diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 06984c35..451f220d 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: [paulocastellano] +github: [trypostit] diff --git a/app/Enums/PostHog/BillingEvent.php b/app/Enums/PostHog/BillingEvent.php new file mode 100644 index 00000000..85c71407 --- /dev/null +++ b/app/Enums/PostHog/BillingEvent.php @@ -0,0 +1,12 @@ +}> $calls + * @param array $payload */ public function __construct( - public array $calls, + public string $method, + public array $payload, ) { $this->onQueue('posthog'); } @@ -45,14 +45,12 @@ public function handle(): void return; } - foreach ($this->calls as $call) { - match (data_get($call, 'method')) { - 'capture' => PostHog::capture(data_get($call, 'payload')), - 'identify' => PostHog::identify(data_get($call, 'payload')), - 'groupIdentify' => PostHog::groupIdentify(data_get($call, 'payload')), - default => Log::warning('PostHog SendEvent: unknown method', ['method' => data_get($call, 'method')]), - }; - } + match ($this->method) { + 'capture' => PostHog::capture($this->payload), + 'identify' => PostHog::identify($this->payload), + 'groupIdentify' => PostHog::groupIdentify($this->payload), + default => Log::warning('PostHog SendEvent: unknown method', ['method' => $this->method]), + }; PostHog::flush(); } diff --git a/app/Jobs/PostHog/SyncUser.php b/app/Jobs/PostHog/SyncUser.php index 34ae719a..7d3290b8 100644 --- a/app/Jobs/PostHog/SyncUser.php +++ b/app/Jobs/PostHog/SyncUser.php @@ -43,7 +43,10 @@ public function __construct(public string $userId) public function handle(PostHogService $postHog): void { - $user = User::with(['account.plan', 'currentWorkspace'])->find($this->userId); + $user = User::with([ + 'account.plan', + 'currentWorkspace' => fn ($query) => $query->withCount('socialAccounts'), + ])->find($this->userId); if (! $user) { return; @@ -78,8 +81,7 @@ public function handle(PostHogService $postHog): void $postHog->groupIdentify('workspace', (string) $workspace->id, [ 'name' => $workspace->name, 'account_id' => (string) $workspace->account_id, - 'social_accounts_count' => $workspace->socialAccounts()->count(), - 'posts_count' => $workspace->posts()->count(), + 'social_accounts_count' => (int) $workspace->social_accounts_count, 'created_at' => $workspace->created_at?->toIso8601String(), ]); } diff --git a/app/Jobs/PostHog/TrackBilling.php b/app/Jobs/PostHog/TrackBilling.php index aa4f9ce2..799539ed 100644 --- a/app/Jobs/PostHog/TrackBilling.php +++ b/app/Jobs/PostHog/TrackBilling.php @@ -4,6 +4,7 @@ namespace App\Jobs\PostHog; +use App\Enums\PostHog\BillingEvent; use App\Models\Account; use App\Services\PostHogService; use Illuminate\Bus\Queueable; @@ -33,7 +34,7 @@ class TrackBilling implements ShouldQueue */ public function __construct( public string $accountId, - public string $event, + public BillingEvent $event, public array $payload, public ?string $previousPlan = null, ) { @@ -50,7 +51,7 @@ public function handle(PostHogService $postHog): void $postHog->capture( (string) $account->owner_id, - $this->event, + $this->event->value, [ 'stripe_status' => data_get($this->payload, 'data.object.status'), 'plan' => $account->plan?->name, diff --git a/app/Listeners/StripeEventListener.php b/app/Listeners/StripeEventListener.php index 6fc0f039..5937f0f4 100644 --- a/app/Listeners/StripeEventListener.php +++ b/app/Listeners/StripeEventListener.php @@ -4,6 +4,7 @@ namespace App\Listeners; +use App\Enums\PostHog\BillingEvent; use App\Events\SubscriptionCreated; use App\Jobs\PostHog\TrackBilling; use App\Models\Account; @@ -49,15 +50,14 @@ public function handle(WebhookReceived $event): void protected function handleSubscriptionCreated(Account $account, array $payload): void { $previousPlan = $account->plan?->name; - $plan = $this->resolvePlanFromSubscriptionItems($payload, $account); - if ($plan && $account->plan_id !== $plan->id) { + if ($plan = $this->resolvePlanFromSubscriptionItems($payload, $account)) { $account->update(['plan_id' => $plan->id]); } SubscriptionCreated::dispatch($account); - $this->trackPlanChange($account, 'subscription.created', $previousPlan, $payload); + $this->trackPlanChange($account, BillingEvent::Created, $previousPlan, $payload); } /** @@ -72,13 +72,12 @@ protected function handleSubscriptionCreated(Account $account, array $payload): protected function handleSubscriptionUpdated(Account $account, array $payload): void { $previousPlan = $account->plan?->name; - $plan = $this->resolvePlanFromSubscriptionItems($payload, $account); - if ($plan && $account->plan_id !== $plan->id) { + if ($plan = $this->resolvePlanFromSubscriptionItems($payload, $account)) { $account->update(['plan_id' => $plan->id]); } - $this->trackPlanChange($account, 'subscription.updated', $previousPlan, $payload); + $this->trackPlanChange($account, BillingEvent::Updated, $previousPlan, $payload); } /** @@ -93,11 +92,9 @@ protected function handleSubscriptionDeleted(Account $account, array $payload): { $previousPlan = $account->plan?->name; - if ($account->plan_id !== null) { - $account->update(['plan_id' => null]); - } + $account->update(['plan_id' => null]); - $this->trackPlanChange($account, 'subscription.cancelled', $previousPlan, $payload); + $this->trackPlanChange($account, BillingEvent::Cancelled, $previousPlan, $payload); } /** @@ -141,7 +138,7 @@ private function resolvePlanFromSubscriptionItems(array $payload, Account $accou * * @param array $payload */ - private function trackPlanChange(Account $account, string $event, ?string $previousPlan, array $payload): void + private function trackPlanChange(Account $account, BillingEvent $event, ?string $previousPlan, array $payload): void { TrackBilling::dispatch((string) $account->id, $event, $payload, $previousPlan); } diff --git a/app/Services/PostHogService.php b/app/Services/PostHogService.php index 23e3214b..7c46c214 100644 --- a/app/Services/PostHogService.php +++ b/app/Services/PostHogService.php @@ -19,7 +19,7 @@ class PostHogService */ public function capture(string $distinctId, string $event, array $properties = [], ?Account $account = null): void { - if (! config('services.posthog.api_key')) { + if (! $this->shouldSend()) { return; } @@ -43,7 +43,7 @@ public function capture(string $distinctId, string $event, array $properties = [ */ public function identify(string $distinctId, array $properties = []): void { - if (! config('services.posthog.api_key')) { + if (! $this->shouldSend()) { return; } @@ -58,7 +58,7 @@ public function identify(string $distinctId, array $properties = []): void */ public function groupIdentify(string $groupType, string $groupKey, array $properties = []): void { - if (! config('services.posthog.api_key')) { + if (! $this->shouldSend()) { return; } @@ -69,15 +69,18 @@ public function groupIdentify(string $groupType, string $groupKey, array $proper ]); } + private function shouldSend(): bool + { + return (bool) config('services.posthog.api_key'); + } + /** * @param array $payload */ private function dispatch(string $method, array $payload): void { try { - SendEvent::dispatch([ - ['method' => $method, 'payload' => $payload], - ]); + SendEvent::dispatch($method, $payload); } catch (\Throwable $e) { Log::warning('PostHogService: failed to dispatch event', ['method' => $method, 'error' => $e->getMessage()]); } diff --git a/resources/js/app.ts b/resources/js/app.ts index 6b54db35..c63c45ea 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -9,7 +9,7 @@ import { createApp, h } from 'vue'; import { initializeDataLayer } from './datalayer'; import dayjs from './dayjs'; -import { capturePageview, syncPostHogContext } from './posthog'; +import { capturePageview, initializePostHog, syncPostHogContext } from './posthog'; import type { Auth } from './types'; configureEcho({ @@ -48,6 +48,7 @@ createInertiaApp({ // The same hooks fire on every Inertia navigation below so the // account group counts stay reactive and workspace switches // re-attach the right workspace group. + initializePostHog(); syncPostHogContext(props.initialPage); capturePageview(); diff --git a/resources/js/composables/useFeatureAccess.ts b/resources/js/composables/useFeatureAccess.ts index 20b00ffb..b87ef7d9 100644 --- a/resources/js/composables/useFeatureAccess.ts +++ b/resources/js/composables/useFeatureAccess.ts @@ -1,21 +1,14 @@ import { usePage } from '@inertiajs/vue3'; import { computed } from 'vue'; +import type { Usage } from '@/types'; + interface Plan { id: string; slug: string; name: string; } -interface Usage { - workspaceCount: number; - socialAccountCount: number; - memberCount: number; - pendingInviteCount: number; - postCount: number; - creditsUsed: number; -} - interface Features { workspaceLimit: number; socialAccountLimit: number; diff --git a/resources/js/posthog.ts b/resources/js/posthog.ts index 11e61d13..11f818a8 100644 --- a/resources/js/posthog.ts +++ b/resources/js/posthog.ts @@ -1,12 +1,16 @@ import type { Page } from '@inertiajs/core'; import posthog from 'posthog-js'; -import type { Auth } from './types'; +import type { Auth, Usage } from './types'; const apiKey = import.meta.env.VITE_POSTHOG_API_KEY as string | undefined; const host = import.meta.env.VITE_POSTHOG_HOST as string | undefined; -if (apiKey) { +export const initializePostHog = (): void => { + if (!apiKey) { + return; + } + posthog.init(apiKey, { api_host: host || 'https://us.i.posthog.com', ui_host: 'https://us.posthog.com', @@ -19,16 +23,7 @@ if (apiKey) { maskTextSelector: '.ph-no-capture', }, }); -} - -interface Usage { - workspaceCount: number; - socialAccountCount: number; - memberCount: number; - pendingInviteCount: number; - postCount: number; - creditsUsed: number; -} +}; /** * Identify the current person and refresh the dual group context (account + diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index 6892b075..483026bc 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -35,6 +35,15 @@ export interface Auth { currentPriceId: string | null; } +export interface Usage { + workspaceCount: number; + socialAccountCount: number; + memberCount: number; + pendingInviteCount: number; + postCount: number; + creditsUsed: number; +} + export interface FlashData { banner?: string; bannerStyle?: 'success' | 'danger' | 'info' | 'warning'; diff --git a/tests/Feature/Jobs/PostHog/SendEventTest.php b/tests/Feature/Jobs/PostHog/SendEventTest.php index 0c93aaa9..ef41f3e8 100644 --- a/tests/Feature/Jobs/PostHog/SendEventTest.php +++ b/tests/Feature/Jobs/PostHog/SendEventTest.php @@ -5,9 +5,7 @@ use App\Jobs\PostHog\SendEvent; test('job is queued on posthog queue', function () { - $job = new SendEvent([ - ['method' => 'capture', 'payload' => ['distinctId' => 'user-1', 'event' => 'test']], - ]); + $job = new SendEvent('capture', ['distinctId' => 'user-1', 'event' => 'test']); expect($job->queue)->toBe('posthog'); }); @@ -15,9 +13,7 @@ test('job skips execution when api key is missing', function () { config(['services.posthog.api_key' => null]); - $job = new SendEvent([ - ['method' => 'capture', 'payload' => ['distinctId' => 'user-1', 'event' => 'test']], - ]); + $job = new SendEvent('capture', ['distinctId' => 'user-1', 'event' => 'test']); // Should not throw - silently skips $job->handle(); @@ -26,7 +22,7 @@ }); test('job has correct retry and timeout settings', function () { - $job = new SendEvent([]); + $job = new SendEvent('capture', []); expect($job->tries)->toBe(3); expect($job->timeout)->toBe(15); diff --git a/tests/Feature/Jobs/PostHog/SyncUserTest.php b/tests/Feature/Jobs/PostHog/SyncUserTest.php index c4c2620e..74d9608f 100644 --- a/tests/Feature/Jobs/PostHog/SyncUserTest.php +++ b/tests/Feature/Jobs/PostHog/SyncUserTest.php @@ -47,13 +47,11 @@ (new SyncUser((string) $this->user->id))->handle(app(PostHogService::class)); Queue::assertPushed(SendEvent::class, function ($job) { - $call = $job->calls[0]; - - return $call['method'] === 'identify' - && $call['payload']['distinctId'] === (string) $this->user->id - && $call['payload']['properties']['email'] === $this->user->email - && $call['payload']['properties']['name'] === $this->user->name - && isset($call['payload']['properties']['$set_once']['signed_up_at']); + return $job->method === 'identify' + && $job->payload['distinctId'] === (string) $this->user->id + && $job->payload['properties']['email'] === $this->user->email + && $job->payload['properties']['name'] === $this->user->name + && isset($job->payload['properties']['$set_once']['signed_up_at']); }); }); @@ -73,17 +71,13 @@ (new SyncUser((string) $this->user->id))->handle(app(PostHogService::class)); Queue::assertPushed(SendEvent::class, function ($job) { - $accountCall = collect($job->calls) - ->merge(collect(Queue::pushed(SendEvent::class))->flatMap(fn ($j) => $j->calls)) - ->first(fn ($c) => $c['method'] === 'groupIdentify' && $c['payload']['groupType'] === 'account'); - - if (! $accountCall) { + if ($job->method !== 'groupIdentify' || $job->payload['groupType'] !== 'account') { return false; } - $props = $accountCall['payload']['properties']; + $props = $job->payload['properties']; - return $accountCall['payload']['groupKey'] === (string) $this->account->id + return $job->payload['groupKey'] === (string) $this->account->id && $props['workspaces_count'] === 1 && $props['social_accounts_count'] === 2 && $props['posts_count'] === 3 @@ -106,15 +100,13 @@ (new SyncUser((string) $this->user->id))->handle(app(PostHogService::class)); - $allCalls = collect(Queue::pushed(SendEvent::class))->flatMap(fn ($j) => $j->calls); - $workspaceCall = $allCalls->first( - fn ($c) => $c['method'] === 'groupIdentify' && $c['payload']['groupType'] === 'workspace', - ); - - expect($workspaceCall)->not->toBeNull(); - expect($workspaceCall['payload']['groupKey'])->toBe((string) $workspace->id); - expect($workspaceCall['payload']['properties']['account_id'])->toBe((string) $this->account->id); - expect($workspaceCall['payload']['properties']['social_accounts_count'])->toBe(1); + Queue::assertPushed(SendEvent::class, function ($job) use ($workspace) { + return $job->method === 'groupIdentify' + && $job->payload['groupType'] === 'workspace' + && $job->payload['groupKey'] === (string) $workspace->id + && $job->payload['properties']['account_id'] === (string) $this->account->id + && $job->payload['properties']['social_accounts_count'] === 1; + }); }); test('handle skips workspace group identify when user has no current workspace', function () { @@ -124,12 +116,9 @@ (new SyncUser((string) $this->user->id))->handle(app(PostHogService::class)); - $allCalls = collect(Queue::pushed(SendEvent::class))->flatMap(fn ($j) => $j->calls); - $workspaceCall = $allCalls->first( - fn ($c) => $c['method'] === 'groupIdentify' && $c['payload']['groupType'] === 'workspace', - ); - - expect($workspaceCall)->toBeNull(); + Queue::assertNotPushed(SendEvent::class, function ($job) { + return $job->method === 'groupIdentify' && $job->payload['groupType'] === 'workspace'; + }); }); test('job is queued on the posthog connection queue', function () { diff --git a/tests/Feature/Jobs/PostHog/TrackBillingTest.php b/tests/Feature/Jobs/PostHog/TrackBillingTest.php index 974ad67b..faee87ce 100644 --- a/tests/Feature/Jobs/PostHog/TrackBillingTest.php +++ b/tests/Feature/Jobs/PostHog/TrackBillingTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use App\Enums\PostHog\BillingEvent; use App\Jobs\PostHog\SendEvent; use App\Jobs\PostHog\SyncUser; use App\Jobs\PostHog\TrackBilling; @@ -28,7 +29,7 @@ }); test('job is queued on the posthog queue', function () { - $job = new TrackBilling((string) $this->account->id, 'subscription.updated', $this->payload); + $job = new TrackBilling((string) $this->account->id, BillingEvent::Updated, $this->payload); expect($job->queue)->toBe('posthog'); }); @@ -36,36 +37,34 @@ test('handle captures event on the owner profile with account group attached', function () { Queue::fake(); - (new TrackBilling((string) $this->account->id, 'subscription.updated', $this->payload)) + (new TrackBilling((string) $this->account->id, BillingEvent::Updated, $this->payload)) ->handle(app(PostHogService::class)); Queue::assertPushed(SendEvent::class, function ($job) { - $call = $job->calls[0]; - - return $call['method'] === 'capture' - && $call['payload']['event'] === 'subscription.updated' - && $call['payload']['distinctId'] === (string) $this->user->id - && $call['payload']['properties']['$groups']['account'] === (string) $this->account->id - && $call['payload']['properties']['stripe_status'] === 'active' - && array_key_exists('previous_plan', $call['payload']['properties']); + return $job->method === 'capture' + && $job->payload['event'] === BillingEvent::Updated->value + && $job->payload['distinctId'] === (string) $this->user->id + && $job->payload['properties']['$groups']['account'] === (string) $this->account->id + && $job->payload['properties']['stripe_status'] === 'active' + && array_key_exists('previous_plan', $job->payload['properties']); }); }); test('handle forwards previousPlan as a property when supplied', function () { Queue::fake(); - (new TrackBilling((string) $this->account->id, 'subscription.updated', $this->payload, 'Starter')) + (new TrackBilling((string) $this->account->id, BillingEvent::Updated, $this->payload, 'Starter')) ->handle(app(PostHogService::class)); Queue::assertPushed(SendEvent::class, function ($job) { - return $job->calls[0]['payload']['properties']['previous_plan'] === 'Starter'; + return $job->payload['properties']['previous_plan'] === 'Starter'; }); }); test('handle dispatches SyncUser for the account owner', function () { Bus::fake([SyncUser::class]); - (new TrackBilling((string) $this->account->id, 'subscription.cancelled', $this->payload)) + (new TrackBilling((string) $this->account->id, BillingEvent::Cancelled, $this->payload)) ->handle(app(PostHogService::class)); Bus::assertDispatched( @@ -78,7 +77,7 @@ Queue::fake(); Bus::fake([SyncUser::class]); - (new TrackBilling('00000000-0000-0000-0000-000000000000', 'subscription.created', $this->payload)) + (new TrackBilling('00000000-0000-0000-0000-000000000000', BillingEvent::Created, $this->payload)) ->handle(app(PostHogService::class)); Queue::assertNothingPushed(); @@ -90,7 +89,7 @@ Queue::fake(); Bus::fake([SyncUser::class]); - (new TrackBilling((string) $this->account->id, 'subscription.created', $this->payload)) + (new TrackBilling((string) $this->account->id, BillingEvent::Created, $this->payload)) ->handle(app(PostHogService::class)); Queue::assertNothingPushed(); @@ -101,7 +100,7 @@ config(['services.posthog.api_key' => null]); Queue::fake(); - (new TrackBilling((string) $this->account->id, 'subscription.created', $this->payload)) + (new TrackBilling((string) $this->account->id, BillingEvent::Created, $this->payload)) ->handle(app(PostHogService::class)); // SyncUser can still be queued but it would itself no-op when handled. diff --git a/tests/Feature/Listeners/StripeEventListenerTest.php b/tests/Feature/Listeners/StripeEventListenerTest.php index 67c61705..aab99c10 100644 --- a/tests/Feature/Listeners/StripeEventListenerTest.php +++ b/tests/Feature/Listeners/StripeEventListenerTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use App\Enums\PostHog\BillingEvent; use App\Events\SubscriptionCreated; use App\Jobs\PostHog\TrackBilling; use App\Listeners\StripeEventListener; @@ -160,7 +161,7 @@ Bus::assertDispatched( TrackBilling::class, fn ($job) => $job->accountId === (string) $this->account->id - && $job->event === 'subscription.created', + && $job->event === BillingEvent::Created, ); }); @@ -174,7 +175,7 @@ Bus::assertDispatched( TrackBilling::class, - fn ($job) => $job->event === 'subscription.updated', + fn ($job) => $job->event === BillingEvent::Updated, ); }); @@ -188,7 +189,7 @@ Bus::assertDispatched( TrackBilling::class, - fn ($job) => $job->event === 'subscription.cancelled', + fn ($job) => $job->event === BillingEvent::Cancelled, ); }); @@ -354,7 +355,7 @@ Bus::assertDispatched( TrackBilling::class, - fn ($job) => $job->event === 'subscription.updated' && $job->previousPlan === $starter->name, + fn ($job) => $job->event === BillingEvent::Updated && $job->previousPlan === $starter->name, ); }); @@ -371,7 +372,7 @@ Bus::assertDispatched( TrackBilling::class, - fn ($job) => $job->event === 'subscription.cancelled' && $job->previousPlan === $starter->name, + fn ($job) => $job->event === BillingEvent::Cancelled && $job->previousPlan === $starter->name, ); }); @@ -390,6 +391,6 @@ Bus::assertDispatched( TrackBilling::class, - fn ($job) => $job->event === 'subscription.created' && $job->previousPlan === null, + fn ($job) => $job->event === BillingEvent::Created && $job->previousPlan === null, ); }); diff --git a/tests/Feature/StripeEventListenerTest.php b/tests/Feature/StripeEventListenerTest.php deleted file mode 100644 index d61170be..00000000 --- a/tests/Feature/StripeEventListenerTest.php +++ /dev/null @@ -1,70 +0,0 @@ - true]); - - $this->account = Account::factory()->create([ - 'stripe_id' => 'cus_test_'.fake()->uuid(), - ]); - $this->user = User::factory()->create([ - 'account_id' => $this->account->id, - ]); - $this->account->update(['owner_id' => $this->user->id]); - $this->workspace = Workspace::factory()->create([ - 'user_id' => $this->user->id, - 'account_id' => $this->account->id, - ]); - $this->workspace->members()->attach($this->user->id, ['role' => Role::Member->value]); - $this->user->update(['current_workspace_id' => $this->workspace->id]); -}); - -test('webhook dispatches subscription created event', function () { - Event::fake([SubscriptionCreated::class]); - - $payload = [ - 'type' => 'customer.subscription.created', - 'data' => [ - 'object' => [ - 'customer' => $this->account->stripe_id, - 'id' => 'sub_test_'.fake()->uuid(), - 'status' => 'active', - ], - ], - ]; - - $listener = new StripeEventListener; - $listener->handle(new WebhookReceived($payload)); - - Event::assertDispatched(SubscriptionCreated::class); -}); - -test('webhook ignores unknown stripe customer', function () { - Event::fake([SubscriptionCreated::class]); - - $payload = [ - 'type' => 'customer.subscription.created', - 'data' => [ - 'object' => [ - 'customer' => 'cus_unknown_'.fake()->uuid(), - 'id' => 'sub_test_'.fake()->uuid(), - 'status' => 'active', - ], - ], - ]; - - $listener = new StripeEventListener; - $listener->handle(new WebhookReceived($payload)); - - Event::assertNotDispatched(SubscriptionCreated::class); -}); diff --git a/tests/Unit/PostHogServiceTest.php b/tests/Unit/PostHogServiceTest.php index 9bf0425a..4db925df 100644 --- a/tests/Unit/PostHogServiceTest.php +++ b/tests/Unit/PostHogServiceTest.php @@ -19,9 +19,9 @@ $service->capture('user-123', 'test_event', ['foo' => 'bar']); Queue::assertPushed(SendEvent::class, function ($job) { - return $job->calls[0]['method'] === 'capture' - && $job->calls[0]['payload']['event'] === 'test_event' - && $job->calls[0]['payload']['distinctId'] === 'user-123'; + return $job->method === 'capture' + && $job->payload['event'] === 'test_event' + && $job->payload['distinctId'] === 'user-123'; }); }); @@ -43,8 +43,8 @@ $service->identify('user-123', ['$email' => 'test@example.com']); Queue::assertPushed(SendEvent::class, function ($job) { - return $job->calls[0]['method'] === 'identify' - && $job->calls[0]['payload']['distinctId'] === 'user-123'; + return $job->method === 'identify' + && $job->payload['distinctId'] === 'user-123'; }); }); @@ -66,9 +66,9 @@ $service->groupIdentify('workspace', 'ws-123', ['name' => 'Test Workspace']); Queue::assertPushed(SendEvent::class, function ($job) { - return $job->calls[0]['method'] === 'groupIdentify' - && $job->calls[0]['payload']['groupType'] === 'workspace' - && $job->calls[0]['payload']['groupKey'] === 'ws-123'; + return $job->method === 'groupIdentify' + && $job->payload['groupType'] === 'workspace' + && $job->payload['groupKey'] === 'ws-123'; }); }); @@ -93,7 +93,7 @@ $service->capture('user-123', 'subscription.created', ['stripe_status' => 'active'], $account); Queue::assertPushed(SendEvent::class, function ($job) use ($account, $plan) { - $payload = $job->calls[0]['payload']; + $payload = $job->payload; return $payload['event'] === 'subscription.created' && $payload['properties']['$groups']['account'] === (string) $account->id @@ -111,7 +111,7 @@ $service->capture('user-123', 'page.viewed', ['url' => '/dashboard']); Queue::assertPushed(SendEvent::class, function ($job) { - $properties = $job->calls[0]['payload']['properties']; + $properties = $job->payload['properties']; return ! array_key_exists('$groups', $properties) && ! array_key_exists('account_id', $properties) From f72a97f6766548da66c938de96ef6c4be1172ec5 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 7 May 2026 10:59:48 -0300 Subject: [PATCH 06/15] refactor: explicit Pennant cache reset on plan_id change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the implicit Account::booted() observer with an explicit Account::forgetPlanFeatureCache() method called from each plan_id mutation site (StripeEventListener x3, BillingController x2). Self-hosted installs naturally never reach any of these callsites — Stripe webhooks do not fire and the billing controllers redirect to /calendar before any plan mutation happens — so the Pennant flush is now guaranteed to be a cloud-only operation. Adds integration coverage proving the full chain webhook -> plan_id update -> Pennant flush -> next Feature::value resolves against the new plan limit. --- .../Controllers/App/BillingController.php | 8 ++ app/Listeners/StripeEventListener.php | 15 +++- app/Models/Account.php | 25 +++--- .../Listeners/StripeEventListenerTest.php | 80 +++++++++++++++++++ tests/Feature/Models/AccountModelTest.php | 21 +++-- 5 files changed, 130 insertions(+), 19 deletions(-) diff --git a/app/Http/Controllers/App/BillingController.php b/app/Http/Controllers/App/BillingController.php index 8d346b94..3338e4bb 100644 --- a/app/Http/Controllers/App/BillingController.php +++ b/app/Http/Controllers/App/BillingController.php @@ -72,6 +72,10 @@ public function checkout(Request $request, Plan $plan): SymfonyResponse|Redirect $account->update(['plan_id' => $plan->id]); + if ($account->wasChanged('plan_id')) { + $account->forgetPlanFeatureCache(); + } + return Inertia::location($checkoutSession->url); } @@ -164,6 +168,10 @@ public function swap(Request $request, Plan $plan): RedirectResponse $subscription->swap($priceId); $account->update(['plan_id' => $plan->id]); + if ($account->wasChanged('plan_id')) { + $account->forgetPlanFeatureCache(); + } + return redirect()->route('app.billing.index') ->with('flash.success', __('billing.flash.plan_changed', ['plan' => $plan->name])); } diff --git a/app/Listeners/StripeEventListener.php b/app/Listeners/StripeEventListener.php index 5937f0f4..02f361f8 100644 --- a/app/Listeners/StripeEventListener.php +++ b/app/Listeners/StripeEventListener.php @@ -53,6 +53,10 @@ protected function handleSubscriptionCreated(Account $account, array $payload): if ($plan = $this->resolvePlanFromSubscriptionItems($payload, $account)) { $account->update(['plan_id' => $plan->id]); + + if ($account->wasChanged('plan_id')) { + $account->forgetPlanFeatureCache(); + } } SubscriptionCreated::dispatch($account); @@ -64,8 +68,7 @@ protected function handleSubscriptionCreated(Account $account, array $payload): * Plan changes coming from Stripe (billing portal, dashboard, or our own * `BillingController@swap`) re-arrive here. Re-resolving the plan from * the price ids guarantees the local state matches Stripe even when a - * change happens out-of-band. Pennant feature caches are forgotten - * automatically via Account::booted() when `plan_id` changes. + * change happens out-of-band. * * @param array $payload */ @@ -75,6 +78,10 @@ protected function handleSubscriptionUpdated(Account $account, array $payload): if ($plan = $this->resolvePlanFromSubscriptionItems($payload, $account)) { $account->update(['plan_id' => $plan->id]); + + if ($account->wasChanged('plan_id')) { + $account->forgetPlanFeatureCache(); + } } $this->trackPlanChange($account, BillingEvent::Updated, $previousPlan, $payload); @@ -94,6 +101,10 @@ protected function handleSubscriptionDeleted(Account $account, array $payload): $account->update(['plan_id' => null]); + if ($account->wasChanged('plan_id')) { + $account->forgetPlanFeatureCache(); + } + $this->trackPlanChange($account, BillingEvent::Cancelled, $previousPlan, $payload); } diff --git a/app/Models/Account.php b/app/Models/Account.php index df81576d..56edf84f 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -32,18 +32,21 @@ class Account extends Model 'plan_id', ]; - protected static function booted(): void + /** + * Drop the cached Pennant feature values for the plan-scoped limits so + * the next read resolves against the current plan. Call this right after + * any `plan_id` change. Centralised here (instead of as a model hook) so + * self-hosted installs — where Stripe webhooks never fire and the billing + * controllers redirect away — never trigger this path. + */ + public function forgetPlanFeatureCache(): void { - static::updated(function (Account $account): void { - if ($account->wasChanged('plan_id')) { - Feature::for($account)->forget([ - WorkspaceLimit::class, - SocialAccountLimit::class, - MemberLimit::class, - MonthlyCreditsLimit::class, - ]); - } - }); + Feature::for($this)->forget([ + WorkspaceLimit::class, + SocialAccountLimit::class, + MemberLimit::class, + MonthlyCreditsLimit::class, + ]); } public function owner(): BelongsTo diff --git a/tests/Feature/Listeners/StripeEventListenerTest.php b/tests/Feature/Listeners/StripeEventListenerTest.php index aab99c10..fba305d0 100644 --- a/tests/Feature/Listeners/StripeEventListenerTest.php +++ b/tests/Feature/Listeners/StripeEventListenerTest.php @@ -4,15 +4,18 @@ use App\Enums\PostHog\BillingEvent; use App\Events\SubscriptionCreated; +use App\Features\WorkspaceLimit; use App\Jobs\PostHog\TrackBilling; use App\Listeners\StripeEventListener; use App\Models\Account; use App\Models\Plan; use App\Models\User; use Illuminate\Support\Facades\Bus; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Log; use Laravel\Cashier\Events\WebhookReceived; +use Laravel\Pennant\Feature; beforeEach(function () { $this->account = Account::factory()->create(['stripe_id' => 'cus_test123']); @@ -394,3 +397,80 @@ fn ($job) => $job->event === BillingEvent::Created && $job->previousPlan === null, ); }); + +// ======================================== +// Pennant feature cache invalidation +// ======================================== + +test('subscription updated flushes the pennant cache when the plan changes', function () { + $starter = Plan::query()->where('slug', 'starter')->firstOrFail(); + $pro = Plan::query()->where('slug', 'pro')->firstOrFail(); + + $this->account->update(['plan_id' => $starter->id]); + + // Prime the Pennant cache against the starter limit. + expect(Feature::for($this->account)->value(WorkspaceLimit::class)) + ->toBe($starter->workspace_limit); + + expect(DB::table('features')->where('scope', 'account|'.$this->account->id)->count()) + ->toBeGreaterThan(0); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.updated', + 'data' => ['object' => [ + 'customer' => 'cus_test123', + 'items' => ['data' => [['price' => ['id' => 'price_pro_monthly']]]], + ]], + ])); + + expect(DB::table('features')->where('scope', 'account|'.$this->account->id)->count()) + ->toBe(0); + + expect(Feature::for($this->account->fresh())->value(WorkspaceLimit::class)) + ->toBe($pro->workspace_limit); +}); + +test('subscription deleted flushes the pennant cache', function () { + $starter = Plan::query()->where('slug', 'starter')->firstOrFail(); + $this->account->update(['plan_id' => $starter->id]); + + Feature::for($this->account)->value(WorkspaceLimit::class); + + expect(DB::table('features')->where('scope', 'account|'.$this->account->id)->count()) + ->toBeGreaterThan(0); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.deleted', + 'data' => ['object' => ['customer' => 'cus_test123']], + ])); + + expect(DB::table('features')->where('scope', 'account|'.$this->account->id)->count()) + ->toBe(0); +}); + +test('subscription updated with same plan does not flush the pennant cache', function () { + $starter = Plan::query()->where('slug', 'starter')->firstOrFail(); + $this->account->update(['plan_id' => $starter->id]); + + Feature::for($this->account)->value(WorkspaceLimit::class); + + $cachedRow = DB::table('features') + ->where('scope', 'account|'.$this->account->id) + ->first(); + + expect($cachedRow)->not->toBeNull(); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.updated', + 'data' => ['object' => [ + 'customer' => 'cus_test123', + 'items' => ['data' => [['price' => ['id' => 'price_starter_monthly']]]], + ]], + ])); + + $stillCachedRow = DB::table('features') + ->where('scope', 'account|'.$this->account->id) + ->first(); + + expect($stillCachedRow->id)->toBe($cachedRow->id); +}); diff --git a/tests/Feature/Models/AccountModelTest.php b/tests/Feature/Models/AccountModelTest.php index ec8113b8..100751df 100644 --- a/tests/Feature/Models/AccountModelTest.php +++ b/tests/Feature/Models/AccountModelTest.php @@ -11,27 +11,36 @@ use Illuminate\Support\Facades\DB; use Laravel\Pennant\Feature; -test('booted hook flushes pennant cache when plan_id changes', function () { +test('forgetPlanFeatureCache drops the cached plan-scoped features', function () { $starter = Plan::where('slug', 'starter')->first(); $plus = Plan::where('slug', 'plus')->first(); $account = Account::factory()->create(['plan_id' => $starter->id]); - expect(Feature::for($account)->value(WorkspaceLimit::class))->toBe($starter->workspace_limit); - expect(Feature::for($account)->value(SocialAccountLimit::class))->toBe($starter->social_account_limit); - expect(Feature::for($account)->value(MemberLimit::class))->toBe($starter->member_limit); - expect(Feature::for($account)->value(MonthlyCreditsLimit::class))->toBe($starter->monthly_credits_limit); + // Prime the Pennant cache against the starter plan. + Feature::for($account)->value(WorkspaceLimit::class); + Feature::for($account)->value(SocialAccountLimit::class); + Feature::for($account)->value(MemberLimit::class); + Feature::for($account)->value(MonthlyCreditsLimit::class); + expect(DB::table('features')->where('scope', 'account|'.$account->id)->count()) + ->toBe(4); + + // Move the account to a plan with different limits and forget the cache. $account->update(['plan_id' => $plus->id]); + $account->forgetPlanFeatureCache(); $account->load('plan'); + expect(DB::table('features')->where('scope', 'account|'.$account->id)->count()) + ->toBe(0); + expect(Feature::for($account)->value(WorkspaceLimit::class))->toBe($plus->workspace_limit); expect(Feature::for($account)->value(SocialAccountLimit::class))->toBe($plus->social_account_limit); expect(Feature::for($account)->value(MemberLimit::class))->toBe($plus->member_limit); expect(Feature::for($account)->value(MonthlyCreditsLimit::class))->toBe($plus->monthly_credits_limit); }); -test('booted hook does not flush pennant when other fields change', function () { +test('updating non-plan fields does not flush the pennant cache', function () { $plan = Plan::where('slug', 'plus')->first(); $account = Account::factory()->create(['plan_id' => $plan->id]); From aadfe7d6e80d22118c35c540f68920563795f3ef Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 7 May 2026 11:08:30 -0300 Subject: [PATCH 07/15] fix: do not set plan_id on checkout creation; drop redundant guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues from review: 1. BillingController::checkout was setting plan_id immediately after creating the Stripe Checkout session, before the user actually paid. If the user abandoned checkout, the account ended up with a plan it never paid for. Plan activation is now driven exclusively by the customer.subscription.created webhook, which fires only after a successful payment. 2. The 'if (\$account->wasChanged('plan_id')) { ... }' guards around forgetPlanFeatureCache() were tautological — Eloquent's update() already short-circuits when nothing changed, and Pennant forget() is idempotent, so an extra cache clear when the plan didn't move is harmless. Removing the guards keeps the listener and swap path readable. --- .../Controllers/App/BillingController.php | 11 +------- app/Listeners/StripeEventListener.php | 15 +++-------- .../Listeners/StripeEventListenerTest.php | 27 ------------------- 3 files changed, 4 insertions(+), 49 deletions(-) diff --git a/app/Http/Controllers/App/BillingController.php b/app/Http/Controllers/App/BillingController.php index 3338e4bb..67836c76 100644 --- a/app/Http/Controllers/App/BillingController.php +++ b/app/Http/Controllers/App/BillingController.php @@ -70,12 +70,6 @@ public function checkout(Request $request, Plan $plan): SymfonyResponse|Redirect 'cancel_url' => route('app.subscribe'), ]); - $account->update(['plan_id' => $plan->id]); - - if ($account->wasChanged('plan_id')) { - $account->forgetPlanFeatureCache(); - } - return Inertia::location($checkoutSession->url); } @@ -167,10 +161,7 @@ public function swap(Request $request, Plan $plan): RedirectResponse $subscription->swap($priceId); $account->update(['plan_id' => $plan->id]); - - if ($account->wasChanged('plan_id')) { - $account->forgetPlanFeatureCache(); - } + $account->forgetPlanFeatureCache(); return redirect()->route('app.billing.index') ->with('flash.success', __('billing.flash.plan_changed', ['plan' => $plan->name])); diff --git a/app/Listeners/StripeEventListener.php b/app/Listeners/StripeEventListener.php index 02f361f8..e2634b90 100644 --- a/app/Listeners/StripeEventListener.php +++ b/app/Listeners/StripeEventListener.php @@ -53,10 +53,7 @@ protected function handleSubscriptionCreated(Account $account, array $payload): if ($plan = $this->resolvePlanFromSubscriptionItems($payload, $account)) { $account->update(['plan_id' => $plan->id]); - - if ($account->wasChanged('plan_id')) { - $account->forgetPlanFeatureCache(); - } + $account->forgetPlanFeatureCache(); } SubscriptionCreated::dispatch($account); @@ -78,10 +75,7 @@ protected function handleSubscriptionUpdated(Account $account, array $payload): if ($plan = $this->resolvePlanFromSubscriptionItems($payload, $account)) { $account->update(['plan_id' => $plan->id]); - - if ($account->wasChanged('plan_id')) { - $account->forgetPlanFeatureCache(); - } + $account->forgetPlanFeatureCache(); } $this->trackPlanChange($account, BillingEvent::Updated, $previousPlan, $payload); @@ -100,10 +94,7 @@ protected function handleSubscriptionDeleted(Account $account, array $payload): $previousPlan = $account->plan?->name; $account->update(['plan_id' => null]); - - if ($account->wasChanged('plan_id')) { - $account->forgetPlanFeatureCache(); - } + $account->forgetPlanFeatureCache(); $this->trackPlanChange($account, BillingEvent::Cancelled, $previousPlan, $payload); } diff --git a/tests/Feature/Listeners/StripeEventListenerTest.php b/tests/Feature/Listeners/StripeEventListenerTest.php index fba305d0..92b4c7cb 100644 --- a/tests/Feature/Listeners/StripeEventListenerTest.php +++ b/tests/Feature/Listeners/StripeEventListenerTest.php @@ -447,30 +447,3 @@ expect(DB::table('features')->where('scope', 'account|'.$this->account->id)->count()) ->toBe(0); }); - -test('subscription updated with same plan does not flush the pennant cache', function () { - $starter = Plan::query()->where('slug', 'starter')->firstOrFail(); - $this->account->update(['plan_id' => $starter->id]); - - Feature::for($this->account)->value(WorkspaceLimit::class); - - $cachedRow = DB::table('features') - ->where('scope', 'account|'.$this->account->id) - ->first(); - - expect($cachedRow)->not->toBeNull(); - - $this->listener->handle(new WebhookReceived([ - 'type' => 'customer.subscription.updated', - 'data' => ['object' => [ - 'customer' => 'cus_test123', - 'items' => ['data' => [['price' => ['id' => 'price_starter_monthly']]]], - ]], - ])); - - $stillCachedRow = DB::table('features') - ->where('scope', 'account|'.$this->account->id) - ->first(); - - expect($stillCachedRow->id)->toBe($cachedRow->id); -}); From dad4ab21c316087417284f2d06c40000d6a5dca1 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 7 May 2026 11:35:28 -0300 Subject: [PATCH 08/15] fix: yearly billing interval display in UpgradeDialog and Billing page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UpgradeDialog was reading 'currentPlan.stripe_yearly_price_id' from 'auth.plan', but auth.plan is built by AuthPlanResource which only exposes {id, slug, name, interval} — the price ids are absent. The 'isOnYearly' computation always compared against undefined and returned false, so users on a yearly subscription saw the dialog as if they were on monthly: no current-plan badge match, the toggle was forced visible, and the wrong price/billing label was shown. Switches both UpgradeDialog and the Billing settings page to read auth.plan.interval directly (the authoritative field already present in shared props), drops the now-unused 'currentPriceId' from the Inertia middleware and the Auth type. Yearly is now the default selection on dialog open and the toggle stays visible regardless of current cadence. --- .../Middleware/App/HandleInertiaRequests.php | 2 -- resources/js/components/UpgradeDialog.vue | 17 +++++++---------- resources/js/pages/settings/account/Billing.vue | 17 +++++++++++++---- resources/js/types/index.d.ts | 1 - 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/app/Http/Middleware/App/HandleInertiaRequests.php b/app/Http/Middleware/App/HandleInertiaRequests.php index 1ffcb50f..f17aa60e 100644 --- a/app/Http/Middleware/App/HandleInertiaRequests.php +++ b/app/Http/Middleware/App/HandleInertiaRequests.php @@ -8,7 +8,6 @@ use App\Http\Resources\App\HandleInertiaRequests\AuthPlanResource; use App\Http\Resources\App\HandleInertiaRequests\AuthUserResource; use App\Http\Resources\App\HandleInertiaRequests\AuthWorkspaceResource; -use App\Models\Account; use App\Models\Plan; use Illuminate\Http\Request; use Inertia\Middleware; @@ -45,7 +44,6 @@ public function share(Request $request): array 'account' => $account ? AuthAccountResource::make($account) : null, 'plan' => $account && $account->plan ? AuthPlanResource::make($account, $account->plan) : null, 'hasActiveSubscription' => $account ? $account->hasActiveSubscription() : false, - 'currentPriceId' => $account?->subscription(Account::SUBSCRIPTION_NAME)?->stripe_price, ], 'usage' => $account && ! $isSelfHosted ? $account->usage() : null, 'features' => $account && ! $isSelfHosted ? $account->featureLimits() : null, diff --git a/resources/js/components/UpgradeDialog.vue b/resources/js/components/UpgradeDialog.vue index e5ca83f4..b23dc1a8 100644 --- a/resources/js/components/UpgradeDialog.vue +++ b/resources/js/components/UpgradeDialog.vue @@ -9,6 +9,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp import { useFeatureAccess } from '@/composables/useFeatureAccess'; import { useUpgradeDialog } from '@/composables/useUpgradeDialog'; import { checkout, swap } from '@/routes/app/billing'; +import type { AuthPlan } from '@/types'; type PlanSlug = 'starter' | 'plus' | 'pro' | 'max'; @@ -34,21 +35,17 @@ const { usage } = useFeatureAccess(); const page = usePage(); const plans = computed(() => (page.props.plans as Plan[]) ?? []); -const currentPlan = computed(() => (page.props.auth as { plan: Plan | null }).plan ?? null); -const currentPriceId = computed(() => (page.props.auth as { currentPriceId?: string | null }).currentPriceId ?? null); +const currentPlan = computed(() => (page.props.auth as { plan: AuthPlan | null }).plan ?? null); const hasActiveSubscription = computed(() => Boolean((page.props.auth as { hasActiveSubscription?: boolean }).hasActiveSubscription)); -const isOnYearly = computed(() => { - if (!currentPriceId.value || !currentPlan.value) return false; - return currentPriceId.value === currentPlan.value.stripe_yearly_price_id; -}); +const isOnYearly = computed(() => currentPlan.value?.interval === 'yearly'); -const isYearly = ref(false); +const isYearly = ref(true); const processing = ref(null); watch(open, (isOpen) => { if (isOpen) { - isYearly.value = isOnYearly.value; + isYearly.value = true; } }); @@ -175,8 +172,8 @@ const onOpenChange = (value: boolean) => { - -
+ +
-import { Head } from '@inertiajs/vue3'; +import { Head, usePage } from '@inertiajs/vue3'; import { IconCreditCard, IconDownload, IconFileText, IconSparkles } from '@tabler/icons-vue'; import { trans } from 'laravel-vue-i18n'; import { computed } from 'vue'; @@ -16,6 +16,7 @@ import { settings as settingsHub } from '@/routes/app'; import { edit as accountEdit } from '@/routes/app/account'; import { index as billingIndex, portal } from '@/routes/app/billing'; import { index as usageIndex } from '@/routes/app/usage'; +import type { AuthPlan } from '@/types'; interface Plan { name: string; @@ -59,9 +60,14 @@ const tabs = computed(() => [ { name: 'billing', label: trans('settings.account.tabs.billing'), href: billingIndex().url }, ]); -const monthlyPrice = (slug: string | undefined): string => { +const page = usePage(); +const authPlan = computed(() => (page.props.auth as { plan: AuthPlan | null }).plan ?? null); +const isYearly = computed(() => authPlan.value?.interval === 'yearly'); + +const displayPrice = (slug: string | undefined): string => { if (! slug) return 'Free'; - return trans(`billing.subscribe.prices.${slug}.monthly`); + const key = isYearly.value ? 'yearly_per_month' : 'monthly'; + return trans(`billing.subscribe.prices.${slug}.${key}`); }; const { openUpgrade } = useUpgradeDialog(); @@ -106,9 +112,12 @@ const { openUpgrade } = useUpgradeDialog(); {{ $t('billing.plan.cancelling') }}

- {{ monthlyPrice(plan?.slug) }} + {{ displayPrice(plan?.slug) }} /{{ $t('billing.plan.month') }}

+

+ {{ isYearly ? $t('billing.subscribe.billed_yearly') : $t('billing.subscribe.billed_monthly') }} +

Date: Thu, 7 May 2026 11:35:36 -0300 Subject: [PATCH 09/15] refactor: allow yearly to monthly swaps in BillingController MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the abort_if guard that blocked switching from a yearly billing cadence to monthly. The product decision was reversed — users should be free to move in either direction without going through support. Removes the corresponding 'swap blocks yearly to monthly downgrade' test. --- .../Controllers/App/BillingController.php | 10 ------- tests/Feature/BillingControllerTest.php | 27 ------------------- 2 files changed, 37 deletions(-) diff --git a/app/Http/Controllers/App/BillingController.php b/app/Http/Controllers/App/BillingController.php index 67836c76..3926bac5 100644 --- a/app/Http/Controllers/App/BillingController.php +++ b/app/Http/Controllers/App/BillingController.php @@ -143,16 +143,6 @@ public function swap(Request $request, Plan $plan): RedirectResponse 'Invalid price for this plan', ); - $currentPlan = $account->plan; - $isOnYearly = $currentPlan && $subscription->stripe_price === $currentPlan->stripe_yearly_price_id; - $isTargetMonthly = $priceId === $plan->stripe_monthly_price_id; - - abort_if( - $isOnYearly && $isTargetMonthly, - 422, - 'Cannot downgrade from yearly to monthly billing.', - ); - $authorization = Gate::inspect('swapPlan', [$account, $plan]); if ($authorization->denied()) { diff --git a/tests/Feature/BillingControllerTest.php b/tests/Feature/BillingControllerTest.php index 003f1772..78be2a60 100644 --- a/tests/Feature/BillingControllerTest.php +++ b/tests/Feature/BillingControllerTest.php @@ -244,33 +244,6 @@ ])); }); -test('swap blocks yearly to monthly downgrade', function () { - config(['trypost.self_hosted' => false]); - - $plan = Plan::where('slug', 'max')->first(); - $plan->update([ - 'stripe_monthly_price_id' => 'price_monthly', - 'stripe_yearly_price_id' => 'price_yearly', - ]); - $this->account->update(['plan_id' => $plan->id]); - - $this->user->unsetRelation('account'); - - $this->account->subscriptions()->create([ - 'type' => Account::SUBSCRIPTION_NAME, - 'stripe_id' => 'sub_test_'.fake()->uuid(), - 'stripe_status' => 'active', - 'stripe_price' => 'price_yearly', - ]); - - $response = $this->actingAs($this->user) - ->post(route('app.billing.swap', $plan), [ - 'price_id' => 'price_monthly', - ]); - - $response->assertStatus(422); -}); - test('swap rejects invalid price_id for plan', function () { config(['trypost.self_hosted' => false]); From 94217172077dba84a216cf0f3ef3665c023fa3c9 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 7 May 2026 12:02:28 -0300 Subject: [PATCH 10/15] feat: require username confirmation when disconnecting a social account Passes the account's @username as confirmText to ConfirmDeleteModal so the user has to type it to enable the destructive action. Mirrors the pattern already used elsewhere in the app for high-impact deletes. --- resources/js/pages/accounts/Index.vue | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/resources/js/pages/accounts/Index.vue b/resources/js/pages/accounts/Index.vue index 64c100d2..bec3d356 100644 --- a/resources/js/pages/accounts/Index.vue +++ b/resources/js/pages/accounts/Index.vue @@ -118,8 +118,11 @@ const handleToggle = (accountId: string) => { router.put(toggleAccount.url(accountId), {}, { preserveScroll: true }); }; -const handleDisconnect = (accountId: string) => { - deleteModal.value?.open({ url: disconnectAccount.url(accountId) }); +const handleDisconnect = (account: SocialAccount) => { + deleteModal.value?.open({ + url: disconnectAccount.url(account.id), + confirmText: account.username, + }); }; @@ -257,7 +260,7 @@ const handleDisconnect = (accountId: string) => { {{ $t('accounts.disconnect') }} From cd28ac40250f7dd1bc6379876d2cad604f2a9d3a Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 7 May 2026 12:42:35 -0300 Subject: [PATCH 11/15] feat: explicit POSTHOG_ENABLED gate for self-hosted safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-hosted installs that inherited POSTHOG_API_KEY from an example or older deploy were still seeing SyncUser/SendEvent jobs run because the gate was based on the api key alone. Switches the gate to an explicit 'services.posthog.enabled' flag (env: POSTHOG_ENABLED, default false) and requires both enabled=true AND api_key for tracking to fire. Backend gating: - PostHogService::isEnabled() — single static helper used everywhere. - AppServiceProvider::configurePostHog — skips PostHog::init when off. - CreateUser::execute — does not enqueue SyncUser when off. - SyncUser::handle, TrackBilling::handle, SendEvent::handle — early return before any DB query so the queue worker does no work. Frontend gating: - New VITE_POSTHOG_ENABLED env var mirrored from POSTHOG_ENABLED. - initializePostHog, syncPostHogContext, capturePageview all gated. Tests updated to set both flags on the happy path; adds explicit 'CreateUser does not dispatch SyncUser when PostHog is disabled'. Deploy note: the trypost.it cloud .env must set POSTHOG_ENABLED=true before this branch is merged or analytics will go dark. --- .env.example | 4 +++- app/Actions/User/CreateUser.php | 5 +++- app/Jobs/PostHog/SendEvent.php | 8 ++++--- app/Jobs/PostHog/SyncUser.php | 8 +++++-- app/Jobs/PostHog/TrackBilling.php | 6 ++++- app/Providers/AppServiceProvider.php | 13 ++++++----- app/Services/PostHogService.php | 23 ++++++++++++------- config/services.php | 1 + resources/js/posthog.ts | 14 +++++++++-- tests/Feature/Actions/User/CreateUserTest.php | 16 ++++++++++++- tests/Feature/Jobs/PostHog/SyncUserTest.php | 2 +- .../Feature/Jobs/PostHog/TrackBillingTest.php | 2 +- tests/Unit/PostHogServiceTest.php | 10 ++++---- 13 files changed, 80 insertions(+), 32 deletions(-) diff --git a/.env.example b/.env.example index 21b6c017..4300c9d4 100644 --- a/.env.example +++ b/.env.example @@ -193,7 +193,8 @@ GIPHY_API_KEY= # Google Tag Manager (optional - analytics) GTM_ID= -# PostHog (optional - analytics) +# PostHog (optional - analytics; off by default, self-hosted installs can ignore) +POSTHOG_ENABLED=false POSTHOG_API_KEY= POSTHOG_HOST=https://us.i.posthog.com @@ -203,5 +204,6 @@ VITE_REVERB_APP_KEY="${REVERB_APP_KEY}" VITE_REVERB_HOST="${REVERB_HOST}" VITE_REVERB_PORT="${REVERB_PORT}" VITE_REVERB_SCHEME="${REVERB_SCHEME}" +VITE_POSTHOG_ENABLED="${POSTHOG_ENABLED}" VITE_POSTHOG_API_KEY="${POSTHOG_API_KEY}" VITE_POSTHOG_HOST="${POSTHOG_HOST}" diff --git a/app/Actions/User/CreateUser.php b/app/Actions/User/CreateUser.php index 8ae1e0ca..ae81ae07 100644 --- a/app/Actions/User/CreateUser.php +++ b/app/Actions/User/CreateUser.php @@ -7,6 +7,7 @@ use App\Jobs\PostHog\SyncUser; use App\Models\Account; use App\Models\User; +use App\Services\PostHogService; use Illuminate\Support\Facades\DB; class CreateUser @@ -41,7 +42,9 @@ public static function execute(array $data, array $utmParameters = []): User return $user; }); - SyncUser::dispatch((string) $user->id); + if (PostHogService::isEnabled()) { + SyncUser::dispatch((string) $user->id); + } return $user; } diff --git a/app/Jobs/PostHog/SendEvent.php b/app/Jobs/PostHog/SendEvent.php index 159f0941..e21acbd6 100644 --- a/app/Jobs/PostHog/SendEvent.php +++ b/app/Jobs/PostHog/SendEvent.php @@ -4,6 +4,7 @@ namespace App\Jobs\PostHog; +use App\Services\PostHogService; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -18,8 +19,9 @@ * namespace (`SyncUser`, `TrackBilling`) build the payloads and queue this * job to do the network work. * - * No-op when `POSTHOG_API_KEY` is unset so self-hosted installs are - * unaffected. + * No-op when `POSTHOG_ENABLED` is false (or `POSTHOG_API_KEY` is unset), + * via `PostHogService::isEnabled()`, so self-hosted installs never reach + * the SDK. */ class SendEvent implements ShouldQueue { @@ -41,7 +43,7 @@ public function __construct( public function handle(): void { - if (! config('services.posthog.api_key')) { + if (! PostHogService::isEnabled()) { return; } diff --git a/app/Jobs/PostHog/SyncUser.php b/app/Jobs/PostHog/SyncUser.php index 7d3290b8..2a9a8c19 100644 --- a/app/Jobs/PostHog/SyncUser.php +++ b/app/Jobs/PostHog/SyncUser.php @@ -25,8 +25,8 @@ * - group `account` → Account (billing/plan, parent of workspaces) * - group `workspace` → Workspace (collaboration unit, child of account) * - * No-op when POSTHOG_API_KEY is unset (PostHogService short-circuits), so - * self-hosted installs are unaffected. + * No-op when PostHog is disabled (`POSTHOG_ENABLED=false` or no API key) + * via `PostHogService::isEnabled()`, so self-hosted installs are unaffected. */ class SyncUser implements ShouldQueue { @@ -43,6 +43,10 @@ public function __construct(public string $userId) public function handle(PostHogService $postHog): void { + if (! PostHogService::isEnabled()) { + return; + } + $user = User::with([ 'account.plan', 'currentWorkspace' => fn ($query) => $query->withCount('socialAccounts'), diff --git a/app/Jobs/PostHog/TrackBilling.php b/app/Jobs/PostHog/TrackBilling.php index 799539ed..e1f8d033 100644 --- a/app/Jobs/PostHog/TrackBilling.php +++ b/app/Jobs/PostHog/TrackBilling.php @@ -19,7 +19,7 @@ * group properties (plan, has_active_subscription, is_on_trial) reflect * the new Stripe state. Dispatched by `StripeEventListener`. * - * No-op when `POSTHOG_API_KEY` is unset. + * No-op when PostHog is disabled (`POSTHOG_ENABLED=false` or no API key). */ class TrackBilling implements ShouldQueue { @@ -43,6 +43,10 @@ public function __construct( public function handle(PostHogService $postHog): void { + if (! PostHogService::isEnabled()) { + return; + } + $account = Account::with('plan')->find($this->accountId); if (! $account || ! $account->owner_id) { diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 4f72bd78..dbe4bcea 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -37,6 +37,7 @@ use App\Models\WorkspaceInvite; use App\Models\WorkspaceLabel; use App\Models\WorkspaceSignature; +use App\Services\PostHogService; use App\Services\PostTemplate\Registry as PostTemplateRegistry; use App\Socialite\InstagramProvider; use App\Socialite\LinkedInPageExtendSocialite; @@ -187,13 +188,13 @@ protected function configureMorphMap(): void protected function configurePostHog(): void { - $apiKey = config('services.posthog.api_key'); - - if ($apiKey) { - PostHog::init($apiKey, [ - 'host' => config('services.posthog.host'), - ]); + if (! PostHogService::isEnabled()) { + return; } + + PostHog::init(config('services.posthog.api_key'), [ + 'host' => config('services.posthog.host'), + ]); } protected function configureRateLimiting(): void diff --git a/app/Services/PostHogService.php b/app/Services/PostHogService.php index 7c46c214..cde8fdd4 100644 --- a/app/Services/PostHogService.php +++ b/app/Services/PostHogService.php @@ -10,6 +10,18 @@ class PostHogService { + /** + * Whether PostHog tracking should run at all. Controlled by the explicit + * `POSTHOG_ENABLED` env var so self-hosted installs that inherited an + * example `POSTHOG_API_KEY` from a config snippet do not silently send + * events upstream. The SaaS deployment opts in via `POSTHOG_ENABLED=true`. + */ + public static function isEnabled(): bool + { + return (bool) config('services.posthog.enabled') + && (bool) config('services.posthog.api_key'); + } + /** * Capture an event for the given distinct id. When `$account` is supplied, * the workspace/plan group is auto-attached so the event is filterable in @@ -19,7 +31,7 @@ class PostHogService */ public function capture(string $distinctId, string $event, array $properties = [], ?Account $account = null): void { - if (! $this->shouldSend()) { + if (! self::isEnabled()) { return; } @@ -43,7 +55,7 @@ public function capture(string $distinctId, string $event, array $properties = [ */ public function identify(string $distinctId, array $properties = []): void { - if (! $this->shouldSend()) { + if (! self::isEnabled()) { return; } @@ -58,7 +70,7 @@ public function identify(string $distinctId, array $properties = []): void */ public function groupIdentify(string $groupType, string $groupKey, array $properties = []): void { - if (! $this->shouldSend()) { + if (! self::isEnabled()) { return; } @@ -69,11 +81,6 @@ public function groupIdentify(string $groupType, string $groupKey, array $proper ]); } - private function shouldSend(): bool - { - return (bool) config('services.posthog.api_key'); - } - /** * @param array $payload */ diff --git a/config/services.php b/config/services.php index af7517ce..b6c194cc 100644 --- a/config/services.php +++ b/config/services.php @@ -116,6 +116,7 @@ ], 'posthog' => [ + 'enabled' => (bool) env('POSTHOG_ENABLED', false), 'api_key' => env('POSTHOG_API_KEY'), 'host' => env('POSTHOG_HOST', 'https://us.i.posthog.com'), ], diff --git a/resources/js/posthog.ts b/resources/js/posthog.ts index 11f818a8..f6532b25 100644 --- a/resources/js/posthog.ts +++ b/resources/js/posthog.ts @@ -5,13 +5,16 @@ import type { Auth, Usage } from './types'; const apiKey = import.meta.env.VITE_POSTHOG_API_KEY as string | undefined; const host = import.meta.env.VITE_POSTHOG_HOST as string | undefined; +// Vite stringifies env vars, so explicit equality with 'true' avoids the +// gotcha where 'false' would be truthy. Mirrors backend `POSTHOG_ENABLED`. +const enabled = import.meta.env.VITE_POSTHOG_ENABLED === 'true' && !!apiKey; export const initializePostHog = (): void => { - if (!apiKey) { + if (!enabled) { return; } - posthog.init(apiKey, { + posthog.init(apiKey as string, { api_host: host || 'https://us.i.posthog.com', ui_host: 'https://us.posthog.com', capture_pageview: false, @@ -40,6 +43,10 @@ export const initializePostHog = (): void => { * group `workspace` → collaboration child (carries `account_id`). */ export const syncPostHogContext = (page: Page): void => { + if (!enabled) { + return; + } + const auth = page.props.auth as Auth | undefined; const usage = page.props.usage as Usage | null | undefined; @@ -77,6 +84,9 @@ export const syncPostHogContext = (page: Page): void => { * every navigation (and once at boot for the first page). */ export const capturePageview = (): void => { + if (!enabled) { + return; + } posthog.capture('$pageview', { $current_url: window.location.href }); }; diff --git a/tests/Feature/Actions/User/CreateUserTest.php b/tests/Feature/Actions/User/CreateUserTest.php index eb46dee1..353f47cc 100644 --- a/tests/Feature/Actions/User/CreateUserTest.php +++ b/tests/Feature/Actions/User/CreateUserTest.php @@ -45,7 +45,8 @@ expect(Workspace::count())->toBe(0); }); -test('CreateUser dispatches SyncUser with the new user id', function () { +test('CreateUser dispatches SyncUser with the new user id when PostHog is enabled', function () { + config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']); Bus::fake([SyncUser::class]); $user = CreateUser::execute([ @@ -59,3 +60,16 @@ fn ($job) => $job->userId === (string) $user->id, ); }); + +test('CreateUser does not dispatch SyncUser when PostHog is disabled', function () { + config(['services.posthog.enabled' => false]); + Bus::fake([SyncUser::class]); + + CreateUser::execute([ + 'name' => 'Jane Doe', + 'email' => 'jane.posthog.disabled@example.com', + 'password' => 'secret123', + ]); + + Bus::assertNotDispatched(SyncUser::class); +}); diff --git a/tests/Feature/Jobs/PostHog/SyncUserTest.php b/tests/Feature/Jobs/PostHog/SyncUserTest.php index 74d9608f..ec4fcd41 100644 --- a/tests/Feature/Jobs/PostHog/SyncUserTest.php +++ b/tests/Feature/Jobs/PostHog/SyncUserTest.php @@ -15,7 +15,7 @@ use Illuminate\Support\Str; beforeEach(function () { - config(['services.posthog.api_key' => 'phc_test_key']); + config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']); $this->account = Account::factory()->create([ 'plan_id' => Plan::query()->where('slug', 'starter')->first()?->id, diff --git a/tests/Feature/Jobs/PostHog/TrackBillingTest.php b/tests/Feature/Jobs/PostHog/TrackBillingTest.php index faee87ce..793043f7 100644 --- a/tests/Feature/Jobs/PostHog/TrackBillingTest.php +++ b/tests/Feature/Jobs/PostHog/TrackBillingTest.php @@ -14,7 +14,7 @@ use Illuminate\Support\Facades\Queue; beforeEach(function () { - config(['services.posthog.api_key' => 'phc_test_key']); + config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']); $this->account = Account::factory()->create([ 'plan_id' => Plan::query()->where('slug', 'starter')->first()?->id, diff --git a/tests/Unit/PostHogServiceTest.php b/tests/Unit/PostHogServiceTest.php index 4db925df..babe0f93 100644 --- a/tests/Unit/PostHogServiceTest.php +++ b/tests/Unit/PostHogServiceTest.php @@ -13,7 +13,7 @@ test('capture dispatches job when api key is configured', function () { Queue::fake(); - config(['services.posthog.api_key' => 'phc_test_key']); + config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']); $service = new PostHogService; $service->capture('user-123', 'test_event', ['foo' => 'bar']); @@ -37,7 +37,7 @@ test('identify dispatches job when api key is configured', function () { Queue::fake(); - config(['services.posthog.api_key' => 'phc_test_key']); + config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']); $service = new PostHogService; $service->identify('user-123', ['$email' => 'test@example.com']); @@ -60,7 +60,7 @@ test('group identify dispatches job when api key is configured', function () { Queue::fake(); - config(['services.posthog.api_key' => 'phc_test_key']); + config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']); $service = new PostHogService; $service->groupIdentify('workspace', 'ws-123', ['name' => 'Test Workspace']); @@ -84,7 +84,7 @@ test('capture auto-attaches account groups when account is supplied', function () { Queue::fake(); - config(['services.posthog.api_key' => 'phc_test_key']); + config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']); $plan = Plan::query()->where('slug', 'starter')->first(); $account = Account::factory()->create(['plan_id' => $plan?->id]); @@ -105,7 +105,7 @@ test('capture without account does not attach group properties', function () { Queue::fake(); - config(['services.posthog.api_key' => 'phc_test_key']); + config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']); $service = new PostHogService; $service->capture('user-123', 'page.viewed', ['url' => '/dashboard']); From b709c19862e0a4bac5475962fe8b26476913b890 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 7 May 2026 13:15:36 -0300 Subject: [PATCH 12/15] fix: PostHog property keys, deletion idempotency and full enabled gate Three fixes from a fresh code review: 1. SyncUser identify used 'email' / 'name' instead of the PostHog special person properties '\$email' / '\$name'. The frontend already used the correct keys; the backend identify (sole source for users who sign up but never log in) would have populated only custom properties, leaving the built-in person profile email/name blank in the PostHog UI. 2. handleSubscriptionDeleted now short-circuits when plan_id is already null. Stripe re-delivers webhooks on transient failures, and the prior version would dispatch a duplicate 'subscription.cancelled' event and re-flush the (already empty) Pennant cache on each retry. 3. useTracking composable called posthog.capture directly, bypassing the new enabled gate. While posthog-js queues calls before init (so no events leaked over the network in self-hosted mode), the buffer grew unbounded and would fire all queued events in bulk if init was ever called. Replaced with a gated captureEvent helper exported from posthog.ts. Plus: drop the now-trivial 'updating non-plan fields does not flush the pennant cache' test (no observer to test against), refresh stale doc comments referencing the removed SyncUserToPostHog filename, and add a Bus::assertNotDispatched check to the deletion-idempotency test. --- app/Jobs/PostHog/SyncUser.php | 4 ++-- app/Listeners/StripeEventListener.php | 8 +++++++ resources/js/composables/useTracking.ts | 8 +++---- resources/js/posthog.ts | 15 ++++++++++++- tests/Feature/Jobs/PostHog/SyncUserTest.php | 4 ++-- .../Feature/Jobs/PostHog/TrackBillingTest.php | 4 ++-- .../Listeners/StripeEventListenerTest.php | 9 ++++++++ tests/Feature/Models/AccountModelTest.php | 21 ------------------- 8 files changed, 41 insertions(+), 32 deletions(-) diff --git a/app/Jobs/PostHog/SyncUser.php b/app/Jobs/PostHog/SyncUser.php index 2a9a8c19..f2529333 100644 --- a/app/Jobs/PostHog/SyncUser.php +++ b/app/Jobs/PostHog/SyncUser.php @@ -57,8 +57,8 @@ public function handle(PostHogService $postHog): void } $postHog->identify((string) $user->id, [ - 'email' => $user->email, - 'name' => $user->name, + '$email' => $user->email, + '$name' => $user->name, '$set_once' => ['signed_up_at' => $user->created_at?->toIso8601String()], ]); diff --git a/app/Listeners/StripeEventListener.php b/app/Listeners/StripeEventListener.php index e2634b90..3aecd923 100644 --- a/app/Listeners/StripeEventListener.php +++ b/app/Listeners/StripeEventListener.php @@ -91,6 +91,14 @@ protected function handleSubscriptionUpdated(Account $account, array $payload): */ protected function handleSubscriptionDeleted(Account $account, array $payload): void { + // Stripe re-delivers webhooks on transient failures; if the plan is + // already cleared we skip the rest so the cancellation event isn't + // captured twice and the (idempotent but pointless) cache flush + // doesn't run again. + if ($account->plan_id === null) { + return; + } + $previousPlan = $account->plan?->name; $account->update(['plan_id' => null]); diff --git a/resources/js/composables/useTracking.ts b/resources/js/composables/useTracking.ts index da363f02..c9fb4a78 100644 --- a/resources/js/composables/useTracking.ts +++ b/resources/js/composables/useTracking.ts @@ -1,4 +1,4 @@ -import posthog from '@/posthog'; +import { captureEvent } from '@/posthog'; const push = (data: Record) => { window.dataLayer = window.dataLayer || []; @@ -7,7 +7,7 @@ const push = (data: Record) => { export const useTracking = () => ({ trackSignUp: (authProvider: string) => { - posthog.capture('user.signed_up', { + captureEvent('user.signed_up', { auth_provider: authProvider, }); @@ -18,7 +18,7 @@ export const useTracking = () => ({ }, trackBeginCheckout: (plan: { name: string; interval: string }) => { - posthog.capture('checkout.started', { + captureEvent('checkout.started', { plan_name: plan.name, interval: plan.interval, }); @@ -31,7 +31,7 @@ export const useTracking = () => ({ }, trackPurchase: (plan: { name: string; interval: string }) => { - posthog.capture('checkout.completed', { + captureEvent('checkout.completed', { plan_name: plan.name, interval: plan.interval, }); diff --git a/resources/js/posthog.ts b/resources/js/posthog.ts index f6532b25..6662195d 100644 --- a/resources/js/posthog.ts +++ b/resources/js/posthog.ts @@ -38,7 +38,7 @@ export const initializePostHog = (): void => { * - Workspace switches re-attach the right `workspace` group (the * navigation event carries the fresh `auth.currentWorkspace`). * - * Hierarchy mirrors the backend `app/Jobs/SyncUserToPostHog.php`: + * Hierarchy mirrors the backend `app/Jobs/PostHog/SyncUser.php`: * person → User, group `account` → billing/plan parent, * group `workspace` → collaboration child (carries `account_id`). */ @@ -90,4 +90,17 @@ export const capturePageview = (): void => { posthog.capture('$pageview', { $current_url: window.location.href }); }; +/** + * Gated wrapper around `posthog.capture` for arbitrary domain events. Use + * this from composables/components instead of calling `posthog.capture` + * directly so self-hosted (or otherwise disabled) installs never queue + * events into the SDK buffer. + */ +export const captureEvent = (event: string, properties?: Record): void => { + if (!enabled) { + return; + } + posthog.capture(event, properties); +}; + export default posthog; diff --git a/tests/Feature/Jobs/PostHog/SyncUserTest.php b/tests/Feature/Jobs/PostHog/SyncUserTest.php index ec4fcd41..ad1ef06a 100644 --- a/tests/Feature/Jobs/PostHog/SyncUserTest.php +++ b/tests/Feature/Jobs/PostHog/SyncUserTest.php @@ -49,8 +49,8 @@ Queue::assertPushed(SendEvent::class, function ($job) { return $job->method === 'identify' && $job->payload['distinctId'] === (string) $this->user->id - && $job->payload['properties']['email'] === $this->user->email - && $job->payload['properties']['name'] === $this->user->name + && $job->payload['properties']['$email'] === $this->user->email + && $job->payload['properties']['$name'] === $this->user->name && isset($job->payload['properties']['$set_once']['signed_up_at']); }); }); diff --git a/tests/Feature/Jobs/PostHog/TrackBillingTest.php b/tests/Feature/Jobs/PostHog/TrackBillingTest.php index 793043f7..5e0e941b 100644 --- a/tests/Feature/Jobs/PostHog/TrackBillingTest.php +++ b/tests/Feature/Jobs/PostHog/TrackBillingTest.php @@ -103,7 +103,7 @@ (new TrackBilling((string) $this->account->id, BillingEvent::Created, $this->payload)) ->handle(app(PostHogService::class)); - // SyncUser can still be queued but it would itself no-op when handled. - // The contract of this job is: no PostHog network call without a key. + // The contract of this job is: when PostHog is disabled, handle short- + // circuits before any DB query and no SendEvent reaches the queue. Queue::assertNotPushed(SendEvent::class); }); diff --git a/tests/Feature/Listeners/StripeEventListenerTest.php b/tests/Feature/Listeners/StripeEventListenerTest.php index 92b4c7cb..9caceea9 100644 --- a/tests/Feature/Listeners/StripeEventListenerTest.php +++ b/tests/Feature/Listeners/StripeEventListenerTest.php @@ -183,6 +183,9 @@ }); test('subscription deleted dispatches TrackBilling with subscription.cancelled event', function () { + $starter = Plan::query()->where('slug', 'starter')->firstOrFail(); + $this->account->update(['plan_id' => $starter->id]); + Bus::fake([TrackBilling::class]); $this->listener->handle(new WebhookReceived([ @@ -330,12 +333,18 @@ test('subscription deleted is idempotent when plan_id is already null', function () { $this->account->update(['plan_id' => null]); + Bus::fake([TrackBilling::class]); + $this->listener->handle(new WebhookReceived([ 'type' => 'customer.subscription.deleted', 'data' => ['object' => ['customer' => 'cus_test123']], ])); expect($this->account->fresh()->plan_id)->toBeNull(); + + // Re-delivery of a cancellation must not produce a duplicate + // 'subscription.cancelled' event downstream. + Bus::assertNotDispatched(TrackBilling::class); }); // ======================================== diff --git a/tests/Feature/Models/AccountModelTest.php b/tests/Feature/Models/AccountModelTest.php index 100751df..f0c3a4e0 100644 --- a/tests/Feature/Models/AccountModelTest.php +++ b/tests/Feature/Models/AccountModelTest.php @@ -39,24 +39,3 @@ expect(Feature::for($account)->value(MemberLimit::class))->toBe($plus->member_limit); expect(Feature::for($account)->value(MonthlyCreditsLimit::class))->toBe($plus->monthly_credits_limit); }); - -test('updating non-plan fields does not flush the pennant cache', function () { - $plan = Plan::where('slug', 'plus')->first(); - $account = Account::factory()->create(['plan_id' => $plan->id]); - - Feature::for($account)->value(WorkspaceLimit::class); - - $cachedRow = DB::table('features') - ->where('scope', 'account|'.$account->id) - ->first(); - - expect($cachedRow)->not->toBeNull(); - - $account->update(['name' => 'Updated Name']); - - $stillCachedRow = DB::table('features') - ->where('scope', 'account|'.$account->id) - ->first(); - - expect($stillCachedRow->id)->toBe($cachedRow->id); -}); From 0a9ca9a185f3902dd65e23281d1b6f9a92c99f4d Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 7 May 2026 13:21:58 -0300 Subject: [PATCH 13/15] fix: serialize plan_slug as string and broaden PostHog enabled tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups from the final pre-merge review: 1. SyncUser and TrackBilling shipped \$account->plan?->slug, which is a PlanSlug backed enum. json_encode renders it correctly on the wire, but the queue payload carries an enum instance — anyone introspecting the job (Bus::fake, future workers) and string-comparing the slug would fail silently. Cast to ->slug->value at the call sites. 2. PostHogServiceTest only covered the api_key=null negative case. The primary scenario the gate exists to defend (self-hosted with a stale POSTHOG_API_KEY but POSTHOG_ENABLED=false) was not asserted. Added four tests covering capture/identify/groupIdentify with enabled=false and an api key present, plus a direct truth-table check on isEnabled() requiring both flags. --- app/Jobs/PostHog/SyncUser.php | 2 +- app/Jobs/PostHog/TrackBilling.php | 2 +- tests/Unit/PostHogServiceTest.php | 49 +++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/app/Jobs/PostHog/SyncUser.php b/app/Jobs/PostHog/SyncUser.php index f2529333..aeed8ab8 100644 --- a/app/Jobs/PostHog/SyncUser.php +++ b/app/Jobs/PostHog/SyncUser.php @@ -68,7 +68,7 @@ public function handle(PostHogService $postHog): void $postHog->groupIdentify('account', (string) $account->id, [ 'name' => $account->name, 'plan' => $account->plan?->name, - 'plan_slug' => $account->plan?->slug, + 'plan_slug' => $account->plan?->slug->value, 'has_active_subscription' => $account->hasActiveSubscription(), 'is_on_trial' => $account->isOnTrial(), 'workspaces_count' => $usage['workspaceCount'], diff --git a/app/Jobs/PostHog/TrackBilling.php b/app/Jobs/PostHog/TrackBilling.php index e1f8d033..7c178ee6 100644 --- a/app/Jobs/PostHog/TrackBilling.php +++ b/app/Jobs/PostHog/TrackBilling.php @@ -59,7 +59,7 @@ public function handle(PostHogService $postHog): void [ 'stripe_status' => data_get($this->payload, 'data.object.status'), 'plan' => $account->plan?->name, - 'plan_slug' => $account->plan?->slug, + 'plan_slug' => $account->plan?->slug->value, 'previous_plan' => $this->previousPlan, ], $account, diff --git a/tests/Unit/PostHogServiceTest.php b/tests/Unit/PostHogServiceTest.php index babe0f93..8e128043 100644 --- a/tests/Unit/PostHogServiceTest.php +++ b/tests/Unit/PostHogServiceTest.php @@ -118,3 +118,52 @@ && ! array_key_exists('plan', $properties); }); }); + +// ======================================== +// Self-hosted contract: enabled=false dominates regardless of api key +// ======================================== +// +// These cover the exact scenario the open-source gate exists to defend +// against: a self-hosted install that inherited POSTHOG_API_KEY from an +// example env file but did NOT explicitly opt in via POSTHOG_ENABLED. + +test('capture is a no-op when enabled is false even with an api key set', function () { + Queue::fake(); + config(['services.posthog.enabled' => false, 'services.posthog.api_key' => 'phc_inherited_key']); + + $service = new PostHogService; + $service->capture('user-123', 'test_event'); + + Queue::assertNothingPushed(); +}); + +test('identify is a no-op when enabled is false even with an api key set', function () { + Queue::fake(); + config(['services.posthog.enabled' => false, 'services.posthog.api_key' => 'phc_inherited_key']); + + $service = new PostHogService; + $service->identify('user-123', ['$email' => 'test@example.com']); + + Queue::assertNothingPushed(); +}); + +test('groupIdentify is a no-op when enabled is false even with an api key set', function () { + Queue::fake(); + config(['services.posthog.enabled' => false, 'services.posthog.api_key' => 'phc_inherited_key']); + + $service = new PostHogService; + $service->groupIdentify('account', 'acc-123', ['name' => 'Test']); + + Queue::assertNothingPushed(); +}); + +test('isEnabled requires both enabled and api key', function () { + config(['services.posthog.enabled' => true, 'services.posthog.api_key' => null]); + expect(PostHogService::isEnabled())->toBeFalse(); + + config(['services.posthog.enabled' => false, 'services.posthog.api_key' => 'phc_x']); + expect(PostHogService::isEnabled())->toBeFalse(); + + config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_x']); + expect(PostHogService::isEnabled())->toBeTrue(); +}); From 7a25ca075953e0d376e7a3856c9d37d7329981ac Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 7 May 2026 13:35:39 -0300 Subject: [PATCH 14/15] fix: gate TrackBilling dispatch in StripeEventListener The TrackBilling job was being enqueued on every Stripe webhook event even with POSTHOG_ENABLED=false. handle() short-circuited via the isEnabled() check, so nothing reached PostHog, but the job still consumed queue worker cycles (20-300ms each) on accounts where Stripe fires its frequent customer.subscription.updated events. Adds the missing isEnabled() check at the dispatch site in trackPlanChange, plus the two test cases that would have caught this the first time around (TrackBilling not dispatched when enabled=false, and not dispatched when api_key is missing). Updates the listener test beforeEach to opt the suite into the enabled path so the existing assertDispatched() assertions continue to fire. --- app/Listeners/StripeEventListener.php | 7 +++++ .../Listeners/StripeEventListenerTest.php | 26 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/app/Listeners/StripeEventListener.php b/app/Listeners/StripeEventListener.php index 3aecd923..23480839 100644 --- a/app/Listeners/StripeEventListener.php +++ b/app/Listeners/StripeEventListener.php @@ -9,6 +9,7 @@ use App\Jobs\PostHog\TrackBilling; use App\Models\Account; use App\Models\Plan; +use App\Services\PostHogService; use Illuminate\Support\Facades\Log; use Laravel\Cashier\Events\WebhookReceived; @@ -145,11 +146,17 @@ private function resolvePlanFromSubscriptionItems(array $payload, Account $accou /** * Hand the lifecycle event off to the queue so the listener stays fast. + * Skip dispatch when PostHog is disabled — `TrackBilling::handle` would + * no-op anyway, but enqueuing it still costs queue worker cycles. * * @param array $payload */ private function trackPlanChange(Account $account, BillingEvent $event, ?string $previousPlan, array $payload): void { + if (! PostHogService::isEnabled()) { + return; + } + TrackBilling::dispatch((string) $account->id, $event, $payload, $previousPlan); } } diff --git a/tests/Feature/Listeners/StripeEventListenerTest.php b/tests/Feature/Listeners/StripeEventListenerTest.php index 9caceea9..a9da1b9d 100644 --- a/tests/Feature/Listeners/StripeEventListenerTest.php +++ b/tests/Feature/Listeners/StripeEventListenerTest.php @@ -18,6 +18,8 @@ use Laravel\Pennant\Feature; beforeEach(function () { + config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']); + $this->account = Account::factory()->create(['stripe_id' => 'cus_test123']); $this->user = User::factory()->create([ 'account_id' => $this->account->id, @@ -210,6 +212,30 @@ Bus::assertNotDispatched(TrackBilling::class); }); +test('TrackBilling is not dispatched when PostHog is disabled', function () { + config(['services.posthog.enabled' => false]); + Bus::fake([TrackBilling::class]); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.created', + 'data' => ['object' => ['customer' => 'cus_test123', 'id' => 'sub_123']], + ])); + + Bus::assertNotDispatched(TrackBilling::class); +}); + +test('TrackBilling is not dispatched when api key is missing', function () { + config(['services.posthog.api_key' => null]); + Bus::fake([TrackBilling::class]); + + $this->listener->handle(new WebhookReceived([ + 'type' => 'customer.subscription.created', + 'data' => ['object' => ['customer' => 'cus_test123', 'id' => 'sub_123']], + ])); + + Bus::assertNotDispatched(TrackBilling::class); +}); + // ======================================== // Plan sync — domain logic // ======================================== From cbea8394fcfeb93dbf7cda05626a7fcbd495b9f2 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 7 May 2026 14:03:52 -0300 Subject: [PATCH 15/15] feat: localize OAuth popup callback messages across 12 controllers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User reported the social-account OAuth callback popup ('Threads account connected!') stayed in English regardless of the active locale. The hardcoded message was wired through SocialController and 11 platform controllers (Bluesky, Facebook, Instagram, InstagramFacebook, LinkedIn, LinkedInPage, Mastodon, Pinterest, Threads, TikTok, YouTube), plus the Blade view that the popup renders. Adds an accounts.popup_callback i18n block (en/pt-BR/es) covering: - The popup chrome (title, closing/close-now status text). - Generic success/reconnect messages (one shared 'Account connected!' / 'Account reconnected!' line — the popup already shows a checkmark and lives for ~2s so platform-specific wording wasn't pulling weight). - Error variants (account/page/channel) and contextual edge cases (page not found, no Facebook pages, no YouTube channels, etc.). Updates every popupCallback() callsite to read from these keys, plus the Blade view's title and submessage. Regenerates the JSON locale bundle so laravel-vue-i18n stays in sync. --- .../Controllers/Auth/BlueskyController.php | 2 +- .../Controllers/Auth/FacebookController.php | 22 +++++++++--------- .../Controllers/Auth/InstagramController.php | 8 +++---- .../Auth/InstagramFacebookController.php | 20 ++++++++-------- .../Controllers/Auth/LinkedInController.php | 8 +++---- .../Auth/LinkedInPageController.php | 18 +++++++-------- .../Controllers/Auth/MastodonController.php | 14 +++++------ .../Controllers/Auth/PinterestController.php | 8 +++---- .../Controllers/Auth/SocialController.php | 8 +++---- .../Controllers/Auth/ThreadsController.php | 10 ++++---- .../Controllers/Auth/TikTokController.php | 8 +++---- .../Controllers/Auth/YouTubeController.php | 22 +++++++++--------- lang/en/accounts.php | 23 +++++++++++++++++++ lang/es/accounts.php | 23 +++++++++++++++++++ lang/php_en.json | 2 +- lang/php_es.json | 2 +- lang/php_pt-BR.json | 2 +- lang/pt-BR/accounts.php | 23 +++++++++++++++++++ .../views/auth/social-callback.blade.php | 6 ++--- 19 files changed, 149 insertions(+), 80 deletions(-) diff --git a/app/Http/Controllers/Auth/BlueskyController.php b/app/Http/Controllers/Auth/BlueskyController.php index b29ce169..8ec10b91 100644 --- a/app/Http/Controllers/Auth/BlueskyController.php +++ b/app/Http/Controllers/Auth/BlueskyController.php @@ -112,7 +112,7 @@ public function store(Request $request): View|RedirectResponse ], ); - return $this->popupCallback(true, 'Bluesky account connected!', $this->platform->value); + return $this->popupCallback(true, __('accounts.popup_callback.connected'), $this->platform->value); } catch (\Exception $e) { Log::error('Bluesky connection error', [ 'error' => $e->getMessage(), diff --git a/app/Http/Controllers/Auth/FacebookController.php b/app/Http/Controllers/Auth/FacebookController.php index a8315515..d91eb252 100644 --- a/app/Http/Controllers/Auth/FacebookController.php +++ b/app/Http/Controllers/Auth/FacebookController.php @@ -64,13 +64,13 @@ public function callback(Request $request): View|RedirectResponse $workspaceId = session('social_connect_workspace'); if (! $workspaceId) { - return $this->popupCallback(false, 'Session expired. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.session_expired'), $this->platform->value); } $workspace = Workspace::find($workspaceId); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { - return $this->popupCallback(false, 'Workspace not found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.workspace_not_found'), $this->platform->value); } try { @@ -87,7 +87,7 @@ public function callback(Request $request): View|RedirectResponse $pages = $this->fetchPages($socialUser->token); if (empty($pages)) { - return $this->popupCallback(false, 'No Facebook Pages found. You need to be an admin of at least one page.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.no_facebook_pages'), $this->platform->value); } // If only one page, connect directly @@ -119,7 +119,7 @@ public function callback(Request $request): View|RedirectResponse ], ); - return $this->popupCallback(true, 'Facebook Page connected!', $this->platform->value); + return $this->popupCallback(true, __('accounts.popup_callback.connected'), $this->platform->value); } // Multiple pages - store data and show selection @@ -138,7 +138,7 @@ public function callback(Request $request): View|RedirectResponse 'trace' => $e->getTraceAsString(), ]); - return $this->popupCallback(false, 'Error connecting account. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.error_connecting'), $this->platform->value); } } @@ -183,20 +183,20 @@ public function select(Request $request): View $workspaceId = session('social_connect_workspace'); if (! $oauthData || ! $workspaceId) { - return $this->popupCallback(false, 'Session expired. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.session_expired'), $this->platform->value); } $workspace = Workspace::find($workspaceId); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { - return $this->popupCallback(false, 'Workspace not found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.workspace_not_found'), $this->platform->value); } try { $selectedPage = collect(data_get($oauthData, 'pages'))->firstWhere('id', $request->page_id); if (! $selectedPage) { - return $this->popupCallback(false, 'Page not found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.page_not_found'), $this->platform->value); } $avatarPath = uploadFromUrl(data_get($selectedPage, 'picture')); @@ -226,7 +226,7 @@ public function select(Request $request): View session()->forget(['facebook_oauth', 'social_reconnect_id']); - return $this->popupCallback(true, 'Facebook Page reconnected!', $this->platform->value); + return $this->popupCallback(true, __('accounts.popup_callback.reconnected'), $this->platform->value); } } @@ -256,13 +256,13 @@ public function select(Request $request): View session()->forget(['facebook_oauth', 'social_reconnect_id']); - return $this->popupCallback(true, 'Facebook Page connected!', $this->platform->value); + return $this->popupCallback(true, __('accounts.popup_callback.connected'), $this->platform->value); } catch (\Exception $e) { Log::error('Facebook page selection error', [ 'error' => $e->getMessage(), ]); - return $this->popupCallback(false, 'Error connecting page. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.error_connecting_page'), $this->platform->value); } } diff --git a/app/Http/Controllers/Auth/InstagramController.php b/app/Http/Controllers/Auth/InstagramController.php index b9379e15..bc667e19 100644 --- a/app/Http/Controllers/Auth/InstagramController.php +++ b/app/Http/Controllers/Auth/InstagramController.php @@ -59,13 +59,13 @@ public function callback(Request $request): View $workspaceId = session('social_connect_workspace'); if (! $workspaceId) { - return $this->popupCallback(false, 'Session expired. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.session_expired'), $this->platform->value); } $workspace = Workspace::find($workspaceId); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { - return $this->popupCallback(false, 'Workspace not found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.workspace_not_found'), $this->platform->value); } try { @@ -100,14 +100,14 @@ public function callback(Request $request): View ], ); - return $this->popupCallback(true, 'Instagram account connected!', $this->platform->value); + return $this->popupCallback(true, __('accounts.popup_callback.connected'), $this->platform->value); } catch (\Exception $e) { Log::error('Instagram OAuth Error', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); - return $this->popupCallback(false, 'Error connecting account. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.error_connecting'), $this->platform->value); } } } diff --git a/app/Http/Controllers/Auth/InstagramFacebookController.php b/app/Http/Controllers/Auth/InstagramFacebookController.php index a2532dab..625ec5dc 100644 --- a/app/Http/Controllers/Auth/InstagramFacebookController.php +++ b/app/Http/Controllers/Auth/InstagramFacebookController.php @@ -67,13 +67,13 @@ public function callback(Request $request): View|RedirectResponse $workspaceId = session('social_connect_workspace'); if (! $workspaceId) { - return $this->popupCallback(false, 'Session expired. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.session_expired'), $this->platform->value); } $workspace = Workspace::find($workspaceId); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { - return $this->popupCallback(false, 'Workspace not found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.workspace_not_found'), $this->platform->value); } $reconnectId = session('social_reconnect_id'); @@ -94,7 +94,7 @@ public function callback(Request $request): View|RedirectResponse $pages = $this->fetchPagesWithInstagram($socialUser->token); if (empty($pages)) { - return $this->popupCallback(false, 'No Facebook Pages with linked Instagram accounts found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.no_facebook_instagram_pages'), $this->platform->value); } if (count($pages) === 1) { @@ -117,7 +117,7 @@ public function callback(Request $request): View|RedirectResponse 'trace' => $e->getTraceAsString(), ]); - return $this->popupCallback(false, 'Error connecting account. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.error_connecting'), $this->platform->value); } } @@ -159,13 +159,13 @@ public function select(Request $request): View $workspaceId = session('social_connect_workspace'); if (! $oauthData || ! $workspaceId) { - return $this->popupCallback(false, 'Session expired. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.session_expired'), $this->platform->value); } $workspace = Workspace::find($workspaceId); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { - return $this->popupCallback(false, 'Workspace not found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.workspace_not_found'), $this->platform->value); } $reconnectId = data_get($oauthData, 'reconnect_id'); @@ -175,7 +175,7 @@ public function select(Request $request): View $selectedPage = collect(data_get($oauthData, 'pages'))->firstWhere('page_id', $request->page_id); if (! $selectedPage) { - return $this->popupCallback(false, 'Page not found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.page_not_found'), $this->platform->value); } $result = $this->connectInstagramAccount($workspace, $selectedPage, $existingAccount); @@ -186,7 +186,7 @@ public function select(Request $request): View } catch (\Exception $e) { Log::error('Instagram via Facebook page selection error', ['error' => $e->getMessage()]); - return $this->popupCallback(false, 'Error connecting account. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.error_connecting'), $this->platform->value); } } @@ -215,7 +215,7 @@ private function connectInstagramAccount(Workspace $workspace, array $pageData, session()->forget('social_reconnect_id'); - return $this->popupCallback(true, 'Instagram account reconnected!', $this->platform->value); + return $this->popupCallback(true, __('accounts.popup_callback.reconnected'), $this->platform->value); } $account = $workspace->socialAccounts()->updateOrCreate( @@ -232,7 +232,7 @@ private function connectInstagramAccount(Workspace $workspace, array $pageData, $isOnboarding = session('social_connect_onboarding', false); - return $this->popupCallback(true, 'Instagram account connected!', $this->platform->value, $isOnboarding); + return $this->popupCallback(true, __('accounts.popup_callback.connected'), $this->platform->value, $isOnboarding); } private function fetchPagesWithInstagram(string $userToken): array diff --git a/app/Http/Controllers/Auth/LinkedInController.php b/app/Http/Controllers/Auth/LinkedInController.php index 5c2e444e..d0de89f6 100644 --- a/app/Http/Controllers/Auth/LinkedInController.php +++ b/app/Http/Controllers/Auth/LinkedInController.php @@ -50,13 +50,13 @@ public function callback(Request $request): View $workspaceId = session('social_connect_workspace'); if (! $workspaceId) { - return $this->popupCallback(false, 'Session expired. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.session_expired'), $this->platform->value); } $workspace = Workspace::find($workspaceId); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { - return $this->popupCallback(false, 'Workspace not found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.workspace_not_found'), $this->platform->value); } try { @@ -95,13 +95,13 @@ public function callback(Request $request): View // Sync tokens to LinkedIn Page if it exists app(LinkedInTokenSynchronizer::class)->syncTokens($account); - return $this->popupCallback(true, 'LinkedIn account connected!', $this->platform->value); + return $this->popupCallback(true, __('accounts.popup_callback.connected'), $this->platform->value); } catch (\Exception $e) { Log::error('LinkedIn OAuth Error', [ 'error' => $e->getMessage(), ]); - return $this->popupCallback(false, 'Error connecting account. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.error_connecting'), $this->platform->value); } } diff --git a/app/Http/Controllers/Auth/LinkedInPageController.php b/app/Http/Controllers/Auth/LinkedInPageController.php index 9baa94cb..109d0ad6 100644 --- a/app/Http/Controllers/Auth/LinkedInPageController.php +++ b/app/Http/Controllers/Auth/LinkedInPageController.php @@ -69,13 +69,13 @@ public function callback(Request $request): View|RedirectResponse $workspaceId = session('social_connect_workspace'); if (! $workspaceId) { - return $this->popupCallback(false, 'Session expired. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.session_expired'), $this->platform->value); } $workspace = Workspace::find($workspaceId); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { - return $this->popupCallback(false, 'Workspace not found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.workspace_not_found'), $this->platform->value); } try { @@ -90,7 +90,7 @@ public function callback(Request $request): View|RedirectResponse $organizations = $this->fetchOrganizations($socialUser->token); if (empty($organizations)) { - return $this->popupCallback(false, 'You are not an administrator of any LinkedIn page.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.not_linkedin_admin'), $this->platform->value); } // Store data in session and redirect to selection page @@ -114,7 +114,7 @@ public function callback(Request $request): View|RedirectResponse 'error' => $e->getMessage(), ]); - return $this->popupCallback(false, 'Error connecting account. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.error_connecting'), $this->platform->value); } } @@ -156,13 +156,13 @@ public function select(Request $request): View $pendingData = session('linkedin_page_pending'); if (! $pendingData) { - return $this->popupCallback(false, 'Session expired. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.session_expired'), $this->platform->value); } $workspace = Workspace::find($pendingData['workspace_id']); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { - return $this->popupCallback(false, 'Workspace not found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.workspace_not_found'), $this->platform->value); } try { @@ -195,7 +195,7 @@ public function select(Request $request): View session()->forget(['linkedin_page_pending', 'linkedin_page_reconnect_id']); - return $this->popupCallback(true, 'LinkedIn Page reconnected!', $this->platform->value); + return $this->popupCallback(true, __('accounts.popup_callback.reconnected'), $this->platform->value); } } @@ -227,13 +227,13 @@ public function select(Request $request): View session()->forget(['linkedin_page_pending', 'linkedin_page_reconnect_id']); - return $this->popupCallback(true, 'LinkedIn Page connected!', $this->platform->value); + return $this->popupCallback(true, __('accounts.popup_callback.connected'), $this->platform->value); } catch (\Exception $e) { Log::error('LinkedIn Page selection error', [ 'error' => $e->getMessage(), ]); - return $this->popupCallback(false, 'Error connecting page. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.error_connecting_page'), $this->platform->value); } } diff --git a/app/Http/Controllers/Auth/MastodonController.php b/app/Http/Controllers/Auth/MastodonController.php index 630286fe..c834e690 100644 --- a/app/Http/Controllers/Auth/MastodonController.php +++ b/app/Http/Controllers/Auth/MastodonController.php @@ -129,13 +129,13 @@ public function callback(Request $request): View if (! $workspaceId || ! $instance) { $this->clearMastodonSession(); - return $this->popupCallback(false, 'Session expired. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.session_expired'), $this->platform->value); } if ($request->state !== $savedState) { $this->clearMastodonSession(); - return $this->popupCallback(false, 'Invalid state. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.invalid_state'), $this->platform->value); } $workspace = Workspace::find($workspaceId); @@ -143,7 +143,7 @@ public function callback(Request $request): View if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { $this->clearMastodonSession(); - return $this->popupCallback(false, 'Workspace not found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.workspace_not_found'), $this->platform->value); } try { @@ -163,7 +163,7 @@ public function callback(Request $request): View ]); $this->clearMastodonSession(); - return $this->popupCallback(false, 'Failed to authenticate.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.failed_to_authenticate'), $this->platform->value); } $tokenData = $tokenResponse->json(); @@ -176,7 +176,7 @@ public function callback(Request $request): View if ($profileResponse->failed()) { $this->clearMastodonSession(); - return $this->popupCallback(false, 'Failed to get profile.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.failed_to_get_profile'), $this->platform->value); } $profile = $profileResponse->json(); @@ -215,7 +215,7 @@ public function callback(Request $request): View $this->clearMastodonSession(); - return $this->popupCallback(true, 'Mastodon account connected!', $this->platform->value); + return $this->popupCallback(true, __('accounts.popup_callback.connected'), $this->platform->value); } catch (\Exception $e) { Log::error('Mastodon callback error', [ 'error' => $e->getMessage(), @@ -223,7 +223,7 @@ public function callback(Request $request): View ]); $this->clearMastodonSession(); - return $this->popupCallback(false, 'Error connecting account.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.error_connecting'), $this->platform->value); } } diff --git a/app/Http/Controllers/Auth/PinterestController.php b/app/Http/Controllers/Auth/PinterestController.php index a1d7f276..cb3be59a 100644 --- a/app/Http/Controllers/Auth/PinterestController.php +++ b/app/Http/Controllers/Auth/PinterestController.php @@ -48,13 +48,13 @@ public function callback(Request $request): View $workspaceId = session('social_connect_workspace'); if (! $workspaceId) { - return $this->popupCallback(false, 'Session expired. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.session_expired'), $this->platform->value); } $workspace = Workspace::find($workspaceId); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { - return $this->popupCallback(false, 'Workspace not found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.workspace_not_found'), $this->platform->value); } try { @@ -83,14 +83,14 @@ public function callback(Request $request): View 'status' => Status::Connected, ]); - return $this->popupCallback(true, 'Pinterest account connected!', $this->platform->value); + return $this->popupCallback(true, __('accounts.popup_callback.connected'), $this->platform->value); } catch (\Exception $e) { Log::error('Pinterest OAuth Error', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); - return $this->popupCallback(false, 'Error connecting account. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.error_connecting'), $this->platform->value); } } } diff --git a/app/Http/Controllers/Auth/SocialController.php b/app/Http/Controllers/Auth/SocialController.php index bab8b82c..6d67ff69 100644 --- a/app/Http/Controllers/Auth/SocialController.php +++ b/app/Http/Controllers/Auth/SocialController.php @@ -167,13 +167,13 @@ protected function handleCallback( $workspaceId = session('social_connect_workspace'); if (! $workspaceId) { - return $this->popupCallback(false, 'Session expired. Please try again.', $platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.session_expired'), $platform->value); } $workspace = Workspace::find($workspaceId); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { - return $this->popupCallback(false, 'Workspace not found.', $platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.workspace_not_found'), $platform->value); } try { @@ -200,14 +200,14 @@ protected function handleCallback( ], ); - return $this->popupCallback(true, 'Account connected!', $platform->value); + return $this->popupCallback(true, __('accounts.popup_callback.connected'), $platform->value); } catch (\Exception $e) { Log::error('Social OAuth Error', [ 'platform' => $platform->value, 'error' => $e->getMessage(), ]); - return $this->popupCallback(false, 'Error connecting account. Please try again.', $platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.error_connecting'), $platform->value); } } diff --git a/app/Http/Controllers/Auth/ThreadsController.php b/app/Http/Controllers/Auth/ThreadsController.php index ede4f2e1..f3c4f418 100644 --- a/app/Http/Controllers/Auth/ThreadsController.php +++ b/app/Http/Controllers/Auth/ThreadsController.php @@ -65,13 +65,13 @@ public function callback(Request $request): View if (! $workspaceId) { session()->forget(['threads_oauth_state', 'social_reconnect_id']); - return $this->popupCallback(false, 'Session expired. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.session_expired'), $this->platform->value); } if ($request->state !== $savedState) { session()->forget(['threads_oauth_state', 'social_reconnect_id']); - return $this->popupCallback(false, 'Invalid state. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.invalid_state'), $this->platform->value); } $workspace = Workspace::find($workspaceId); @@ -79,7 +79,7 @@ public function callback(Request $request): View if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { session()->forget(['threads_oauth_state', 'social_reconnect_id']); - return $this->popupCallback(false, 'Workspace not found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.workspace_not_found'), $this->platform->value); } try { @@ -157,7 +157,7 @@ public function callback(Request $request): View session()->forget(['threads_oauth_state', 'social_reconnect_id']); - return $this->popupCallback(true, 'Threads account connected!', $this->platform->value); + return $this->popupCallback(true, __('accounts.popup_callback.connected'), $this->platform->value); } catch (\Exception $e) { Log::error('Threads OAuth Error', [ 'error' => $e->getMessage(), @@ -166,7 +166,7 @@ public function callback(Request $request): View session()->forget(['threads_oauth_state', 'social_reconnect_id']); - return $this->popupCallback(false, 'Error connecting account. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.error_connecting'), $this->platform->value); } } } diff --git a/app/Http/Controllers/Auth/TikTokController.php b/app/Http/Controllers/Auth/TikTokController.php index e0920f0f..440a6f6d 100644 --- a/app/Http/Controllers/Auth/TikTokController.php +++ b/app/Http/Controllers/Auth/TikTokController.php @@ -51,13 +51,13 @@ public function callback(Request $request): View $workspaceId = session('social_connect_workspace'); if (! $workspaceId) { - return $this->popupCallback(false, 'Session expired. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.session_expired'), $this->platform->value); } $workspace = Workspace::find($workspaceId); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { - return $this->popupCallback(false, 'Workspace not found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.workspace_not_found'), $this->platform->value); } try { @@ -96,13 +96,13 @@ public function callback(Request $request): View session()->forget('social_reconnect_id'); - return $this->popupCallback(true, 'TikTok account connected!', $this->platform->value); + return $this->popupCallback(true, __('accounts.popup_callback.connected'), $this->platform->value); } catch (\Exception $e) { Log::error('TikTok OAuth Error', [ 'error' => $e->getMessage(), ]); - return $this->popupCallback(false, 'Error connecting account. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.error_connecting'), $this->platform->value); } } } diff --git a/app/Http/Controllers/Auth/YouTubeController.php b/app/Http/Controllers/Auth/YouTubeController.php index 3d59d70e..59be9062 100644 --- a/app/Http/Controllers/Auth/YouTubeController.php +++ b/app/Http/Controllers/Auth/YouTubeController.php @@ -56,13 +56,13 @@ public function callback(Request $request): View|RedirectResponse $workspaceId = session('social_connect_workspace'); if (! $workspaceId) { - return $this->popupCallback(false, 'Session expired. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.session_expired'), $this->platform->value); } $workspace = Workspace::find($workspaceId); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { - return $this->popupCallback(false, 'Workspace not found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.workspace_not_found'), $this->platform->value); } try { @@ -72,7 +72,7 @@ public function callback(Request $request): View|RedirectResponse $channels = $this->fetchChannels($socialUser->token); if (empty($channels)) { - return $this->popupCallback(false, 'No YouTube channels found. Please create a channel first.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.no_youtube_channels'), $this->platform->value); } // If only one channel, connect directly (most common case) @@ -103,7 +103,7 @@ public function callback(Request $request): View|RedirectResponse ], ); - return $this->popupCallback(true, 'YouTube channel connected!', $this->platform->value); + return $this->popupCallback(true, __('accounts.popup_callback.connected'), $this->platform->value); } // Multiple channels - store data and show selection screen @@ -123,7 +123,7 @@ public function callback(Request $request): View|RedirectResponse 'trace' => $e->getTraceAsString(), ]); - return $this->popupCallback(false, 'Error connecting account. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.error_connecting'), $this->platform->value); } } @@ -178,13 +178,13 @@ public function select(Request $request): View $workspaceId = session('social_connect_workspace'); if (! $oauthData || ! $workspaceId) { - return $this->popupCallback(false, 'Session expired. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.session_expired'), $this->platform->value); } $workspace = Workspace::find($workspaceId); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { - return $this->popupCallback(false, 'Workspace not found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.workspace_not_found'), $this->platform->value); } try { @@ -192,7 +192,7 @@ public function select(Request $request): View $selectedChannel = collect($channels)->firstWhere('id', $request->channel_id); if (! $selectedChannel) { - return $this->popupCallback(false, 'Channel not found.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.channel_not_found'), $this->platform->value); } $avatarPath = uploadFromUrl(data_get($selectedChannel, 'thumbnail')); @@ -221,7 +221,7 @@ public function select(Request $request): View session()->forget(['youtube_oauth', 'social_reconnect_id']); - return $this->popupCallback(true, 'YouTube channel reconnected!', $this->platform->value); + return $this->popupCallback(true, __('accounts.popup_callback.reconnected'), $this->platform->value); } } @@ -250,13 +250,13 @@ public function select(Request $request): View session()->forget(['youtube_oauth', 'social_reconnect_id']); - return $this->popupCallback(true, 'YouTube channel connected!', $this->platform->value); + return $this->popupCallback(true, __('accounts.popup_callback.connected'), $this->platform->value); } catch (\Exception $e) { Log::error('YouTube channel selection error', [ 'error' => $e->getMessage(), ]); - return $this->popupCallback(false, 'Error connecting channel. Please try again.', $this->platform->value); + return $this->popupCallback(false, __('accounts.popup_callback.error_connecting_channel'), $this->platform->value); } } diff --git a/lang/en/accounts.php b/lang/en/accounts.php index 071f6e54..ef6a4e93 100644 --- a/lang/en/accounts.php +++ b/lang/en/accounts.php @@ -115,4 +115,27 @@ 'already_connected' => 'This platform is already connected.', 'no_youtube_channels' => 'No YouTube channels found. Please create a channel first.', ], + + 'popup_callback' => [ + 'title_success' => 'Connected', + 'title_error' => 'Error', + 'closing' => 'This window will close automatically...', + 'close_now' => 'You can close this window now.', + 'connected' => 'Account connected!', + 'reconnected' => 'Account reconnected!', + 'error_connecting' => 'Error connecting account. Please try again.', + 'error_connecting_page' => 'Error connecting page. Please try again.', + 'error_connecting_channel' => 'Error connecting channel. Please try again.', + 'session_expired' => 'Session expired. Please try again.', + 'workspace_not_found' => 'Workspace not found.', + 'invalid_state' => 'Invalid state. Please try again.', + 'failed_to_authenticate' => 'Failed to authenticate.', + 'failed_to_get_profile' => 'Failed to get profile.', + 'page_not_found' => 'Page not found.', + 'channel_not_found' => 'Channel not found.', + 'no_facebook_pages' => 'No Facebook Pages found. You need to be an admin of at least one page.', + 'no_facebook_instagram_pages' => 'No Facebook Pages with linked Instagram accounts found.', + 'no_youtube_channels' => 'No YouTube channels found. Please create a channel first.', + 'not_linkedin_admin' => 'You are not an administrator of any LinkedIn page.', + ], ]; diff --git a/lang/es/accounts.php b/lang/es/accounts.php index efdf8396..7970b605 100644 --- a/lang/es/accounts.php +++ b/lang/es/accounts.php @@ -115,4 +115,27 @@ 'already_connected' => 'Esta plataforma ya está conectada.', 'no_youtube_channels' => 'No se encontraron canales de YouTube. Crea un canal primero.', ], + + 'popup_callback' => [ + 'title_success' => 'Conectado', + 'title_error' => 'Error', + 'closing' => 'Esta ventana se cerrará automáticamente...', + 'close_now' => 'Puedes cerrar esta ventana ahora.', + 'connected' => '¡Cuenta conectada!', + 'reconnected' => '¡Cuenta reconectada!', + 'error_connecting' => 'Error al conectar la cuenta. Inténtalo de nuevo.', + 'error_connecting_page' => 'Error al conectar la página. Inténtalo de nuevo.', + 'error_connecting_channel' => 'Error al conectar el canal. Inténtalo de nuevo.', + 'session_expired' => 'Sesión expirada. Inténtalo de nuevo.', + 'workspace_not_found' => 'Workspace no encontrado.', + 'invalid_state' => 'Estado inválido. Inténtalo de nuevo.', + 'failed_to_authenticate' => 'Falló la autenticación.', + 'failed_to_get_profile' => 'Falló al obtener el perfil.', + 'page_not_found' => 'Página no encontrada.', + 'channel_not_found' => 'Canal no encontrado.', + 'no_facebook_pages' => 'No se encontraron páginas de Facebook. Debes ser administrador de al menos una página.', + 'no_facebook_instagram_pages' => 'No se encontraron páginas de Facebook con cuentas de Instagram vinculadas.', + 'no_youtube_channels' => 'No se encontraron canales de YouTube. Crea un canal primero.', + 'not_linkedin_admin' => 'No eres administrador de ninguna página de LinkedIn.', + ], ]; diff --git a/lang/php_en.json b/lang/php_en.json index 7e5a37f4..8690b66a 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.","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":"Start with a 7-day free trial. No charge until your trial ends.","billing.subscribe.trial_info":"7-day free trial, then billed automatically","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.start_trial":"Start 7-day free trial","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.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","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.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.tiktok.settings":"TikTok Settings","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_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.branded_blocks_private":"Branded content cannot be private. Choose Public or Mutual follow friends.","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.creator_info_loading":"Loading your TikTok account settings…","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.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.scheduled_for":"Scheduled · :date","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.drag_drop":"Drop files to upload","posts.edit.drag_drop_hint":"Drag & drop files here, or use the buttons above","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.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.youtube_short.label":"Short","posts.content_types.youtube_short.description":"Vertical video up to 60 seconds","posts.content_types.x_post.label":"Post","posts.content_types.x_post.description":"Tweet with text and media","posts.content_types.threads_post.label":"Post","posts.content_types.threads_post.description":"Text post with optional media","posts.content_types.pinterest_pin.label":"Pin","posts.content_types.pinterest_pin.description":"Image pin with link","posts.content_types.pinterest_video_pin.label":"Video Pin","posts.content_types.pinterest_video_pin.description":"Video content","posts.content_types.pinterest_carousel.label":"Carousel","posts.content_types.pinterest_carousel.description":"2-5 images","posts.content_types.bluesky_post.label":"Post","posts.content_types.bluesky_post.description":"Text post with optional images","posts.content_types.mastodon_post.label":"Post","posts.content_types.mastodon_post.description":"Text post with optional media","posts.platforms.linkedin":"LinkedIn","posts.platforms.linkedin-page":"LinkedIn Page","posts.platforms.x":"X","posts.platforms.tiktok":"TikTok","posts.platforms.youtube":"YouTube Shorts","posts.platforms.facebook":"Facebook Page","posts.platforms.instagram":"Instagram","posts.platforms.threads":"Threads","posts.platforms.pinterest":"Pinterest","posts.platforms.bluesky":"Bluesky","posts.platforms.mastodon":"Mastodon","posts.flash.scheduled":"Post scheduled successfully!","posts.flash.publishing":"Post is being published! It may take a few minutes to process and appear on each platform.","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.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_title":"Preview","posts.create.steps.preview_loading":"Generating your content…","posts.create.steps.preview_error":"Something went wrong. Please try again.","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.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.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":"Start with a 7-day free trial. No charge until your trial ends.","billing.subscribe.trial_info":"7-day free trial, then billed automatically","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.start_trial":"Start 7-day free trial","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.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","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.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.tiktok.settings":"TikTok Settings","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_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.branded_blocks_private":"Branded content cannot be private. Choose Public or Mutual follow friends.","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.creator_info_loading":"Loading your TikTok account settings…","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.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.scheduled_for":"Scheduled · :date","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.drag_drop":"Drop files to upload","posts.edit.drag_drop_hint":"Drag & drop files here, or use the buttons above","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.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.youtube_short.label":"Short","posts.content_types.youtube_short.description":"Vertical video up to 60 seconds","posts.content_types.x_post.label":"Post","posts.content_types.x_post.description":"Tweet with text and media","posts.content_types.threads_post.label":"Post","posts.content_types.threads_post.description":"Text post with optional media","posts.content_types.pinterest_pin.label":"Pin","posts.content_types.pinterest_pin.description":"Image pin with link","posts.content_types.pinterest_video_pin.label":"Video Pin","posts.content_types.pinterest_video_pin.description":"Video content","posts.content_types.pinterest_carousel.label":"Carousel","posts.content_types.pinterest_carousel.description":"2-5 images","posts.content_types.bluesky_post.label":"Post","posts.content_types.bluesky_post.description":"Text post with optional images","posts.content_types.mastodon_post.label":"Post","posts.content_types.mastodon_post.description":"Text post with optional media","posts.platforms.linkedin":"LinkedIn","posts.platforms.linkedin-page":"LinkedIn Page","posts.platforms.x":"X","posts.platforms.tiktok":"TikTok","posts.platforms.youtube":"YouTube Shorts","posts.platforms.facebook":"Facebook Page","posts.platforms.instagram":"Instagram","posts.platforms.threads":"Threads","posts.platforms.pinterest":"Pinterest","posts.platforms.bluesky":"Bluesky","posts.platforms.mastodon":"Mastodon","posts.flash.scheduled":"Post scheduled successfully!","posts.flash.publishing":"Post is being published! It may take a few minutes to process and appear on each platform.","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.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_title":"Preview","posts.create.steps.preview_loading":"Generating your content…","posts.create.steps.preview_error":"Something went wrong. Please try again.","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.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.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 30a3fd05..4879105c 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.","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":"Comienza con 7 días gratis. Sin cargo hasta que termine tu prueba.","billing.subscribe.trial_info":"Prueba gratuita de 7 días, luego se cobra automáticamente","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.start_trial":"Comenzar prueba de 7 días","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.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","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.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.tiktok.settings":"Configuración de TikTok","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_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.branded_blocks_private":"El contenido patrocinado no puede ser privado. Elige Público o Amigos mutuos.","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.creator_info_loading":"Cargando la configuración de tu cuenta TikTok…","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.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.scheduled_for":"Programado · :date","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.drag_drop":"Suelta los archivos para subir","posts.edit.drag_drop_hint":"Arrastra archivos aquí o usa los botones de arriba","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.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.youtube_short.label":"Short","posts.content_types.youtube_short.description":"Video vertical de hasta 60 segundos","posts.content_types.x_post.label":"Post","posts.content_types.x_post.description":"Tweet con texto y multimedia","posts.content_types.threads_post.label":"Post","posts.content_types.threads_post.description":"Post de texto con multimedia opcional","posts.content_types.pinterest_pin.label":"Pin","posts.content_types.pinterest_pin.description":"Pin de imagen con enlace","posts.content_types.pinterest_video_pin.label":"Pin de video","posts.content_types.pinterest_video_pin.description":"Contenido de video","posts.content_types.pinterest_carousel.label":"Carrusel","posts.content_types.pinterest_carousel.description":"2-5 imágenes","posts.content_types.bluesky_post.label":"Post","posts.content_types.bluesky_post.description":"Post de texto con imágenes opcionales","posts.content_types.mastodon_post.label":"Post","posts.content_types.mastodon_post.description":"Post de texto con multimedia opcional","posts.platforms.linkedin":"LinkedIn","posts.platforms.linkedin-page":"Página de LinkedIn","posts.platforms.x":"X","posts.platforms.tiktok":"TikTok","posts.platforms.youtube":"YouTube Shorts","posts.platforms.facebook":"Página de Facebook","posts.platforms.instagram":"Instagram","posts.platforms.threads":"Threads","posts.platforms.pinterest":"Pinterest","posts.platforms.bluesky":"Bluesky","posts.platforms.mastodon":"Mastodon","posts.flash.scheduled":"¡Post programado correctamente!","posts.flash.publishing":"¡El post se está publicando! Puede tardar unos minutos en procesarse y aparecer en cada plataforma.","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.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_title":"Vista previa","posts.create.steps.preview_loading":"Generando tu contenido…","posts.create.steps.preview_error":"Algo salió mal. Por favor, inténtalo de nuevo.","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.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.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":"Comienza con 7 días gratis. Sin cargo hasta que termine tu prueba.","billing.subscribe.trial_info":"Prueba gratuita de 7 días, luego se cobra automáticamente","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.start_trial":"Comenzar prueba de 7 días","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.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","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.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.tiktok.settings":"Configuración de TikTok","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_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.branded_blocks_private":"El contenido patrocinado no puede ser privado. Elige Público o Amigos mutuos.","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.creator_info_loading":"Cargando la configuración de tu cuenta TikTok…","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.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.scheduled_for":"Programado · :date","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.drag_drop":"Suelta los archivos para subir","posts.edit.drag_drop_hint":"Arrastra archivos aquí o usa los botones de arriba","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.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.youtube_short.label":"Short","posts.content_types.youtube_short.description":"Video vertical de hasta 60 segundos","posts.content_types.x_post.label":"Post","posts.content_types.x_post.description":"Tweet con texto y multimedia","posts.content_types.threads_post.label":"Post","posts.content_types.threads_post.description":"Post de texto con multimedia opcional","posts.content_types.pinterest_pin.label":"Pin","posts.content_types.pinterest_pin.description":"Pin de imagen con enlace","posts.content_types.pinterest_video_pin.label":"Pin de video","posts.content_types.pinterest_video_pin.description":"Contenido de video","posts.content_types.pinterest_carousel.label":"Carrusel","posts.content_types.pinterest_carousel.description":"2-5 imágenes","posts.content_types.bluesky_post.label":"Post","posts.content_types.bluesky_post.description":"Post de texto con imágenes opcionales","posts.content_types.mastodon_post.label":"Post","posts.content_types.mastodon_post.description":"Post de texto con multimedia opcional","posts.platforms.linkedin":"LinkedIn","posts.platforms.linkedin-page":"Página de LinkedIn","posts.platforms.x":"X","posts.platforms.tiktok":"TikTok","posts.platforms.youtube":"YouTube Shorts","posts.platforms.facebook":"Página de Facebook","posts.platforms.instagram":"Instagram","posts.platforms.threads":"Threads","posts.platforms.pinterest":"Pinterest","posts.platforms.bluesky":"Bluesky","posts.platforms.mastodon":"Mastodon","posts.flash.scheduled":"¡Post programado correctamente!","posts.flash.publishing":"¡El post se está publicando! Puede tardar unos minutos en procesarse y aparecer en cada plataforma.","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.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_title":"Vista previa","posts.create.steps.preview_loading":"Generando tu contenido…","posts.create.steps.preview_error":"Algo salió mal. Por favor, inténtalo de nuevo.","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.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.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 53d8d4a7..3effc6ff 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.","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":"Comece com 7 dias grátis. Sem cobrança até o fim do seu teste.","billing.subscribe.trial_info":"7 dias grátis, depois cobrança automática","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.start_trial":"Iniciar teste de 7 dias","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.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","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.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.tiktok.settings":"Configurações do TikTok","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_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.branded_blocks_private":"Conteúdo patrocinado não pode ser privado. Escolha Público ou Amigos em comum.","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.creator_info_loading":"Carregando configurações da sua conta TikTok…","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.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.scheduled_for":"Agendado · :date","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.drag_drop":"Solte os arquivos para enviar","posts.edit.drag_drop_hint":"Arraste arquivos aqui ou use os botões acima","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.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.youtube_short.label":"Short","posts.content_types.youtube_short.description":"Vídeo vertical de até 60 segundos","posts.content_types.x_post.label":"Post","posts.content_types.x_post.description":"Tweet com texto e mídia","posts.content_types.threads_post.label":"Post","posts.content_types.threads_post.description":"Post de texto com mídia opcional","posts.content_types.pinterest_pin.label":"Pin","posts.content_types.pinterest_pin.description":"Pin de imagem com link","posts.content_types.pinterest_video_pin.label":"Pin de Vídeo","posts.content_types.pinterest_video_pin.description":"Conteúdo em vídeo","posts.content_types.pinterest_carousel.label":"Carrossel","posts.content_types.pinterest_carousel.description":"2-5 imagens","posts.content_types.bluesky_post.label":"Post","posts.content_types.bluesky_post.description":"Post de texto com imagens opcionais","posts.content_types.mastodon_post.label":"Post","posts.content_types.mastodon_post.description":"Post de texto com mídia opcional","posts.platforms.linkedin":"LinkedIn","posts.platforms.linkedin-page":"Página do LinkedIn","posts.platforms.x":"X","posts.platforms.tiktok":"TikTok","posts.platforms.youtube":"YouTube Shorts","posts.platforms.facebook":"Página do Facebook","posts.platforms.instagram":"Instagram","posts.platforms.threads":"Threads","posts.platforms.pinterest":"Pinterest","posts.platforms.bluesky":"Bluesky","posts.platforms.mastodon":"Mastodon","posts.flash.scheduled":"Post agendado com sucesso!","posts.flash.publishing":"Post está sendo publicado! Pode levar alguns minutos para processar e aparecer em cada plataforma.","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.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_title":"Prévia","posts.create.steps.preview_loading":"Gerando seu conteúdo…","posts.create.steps.preview_error":"Algo deu errado. Por favor, tente novamente.","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.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.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":"Comece com 7 dias grátis. Sem cobrança até o fim do seu teste.","billing.subscribe.trial_info":"7 dias grátis, depois cobrança automática","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.start_trial":"Iniciar teste de 7 dias","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.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","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.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.tiktok.settings":"Configurações do TikTok","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_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.branded_blocks_private":"Conteúdo patrocinado não pode ser privado. Escolha Público ou Amigos em comum.","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.creator_info_loading":"Carregando configurações da sua conta TikTok…","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.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.scheduled_for":"Agendado · :date","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.drag_drop":"Solte os arquivos para enviar","posts.edit.drag_drop_hint":"Arraste arquivos aqui ou use os botões acima","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.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.youtube_short.label":"Short","posts.content_types.youtube_short.description":"Vídeo vertical de até 60 segundos","posts.content_types.x_post.label":"Post","posts.content_types.x_post.description":"Tweet com texto e mídia","posts.content_types.threads_post.label":"Post","posts.content_types.threads_post.description":"Post de texto com mídia opcional","posts.content_types.pinterest_pin.label":"Pin","posts.content_types.pinterest_pin.description":"Pin de imagem com link","posts.content_types.pinterest_video_pin.label":"Pin de Vídeo","posts.content_types.pinterest_video_pin.description":"Conteúdo em vídeo","posts.content_types.pinterest_carousel.label":"Carrossel","posts.content_types.pinterest_carousel.description":"2-5 imagens","posts.content_types.bluesky_post.label":"Post","posts.content_types.bluesky_post.description":"Post de texto com imagens opcionais","posts.content_types.mastodon_post.label":"Post","posts.content_types.mastodon_post.description":"Post de texto com mídia opcional","posts.platforms.linkedin":"LinkedIn","posts.platforms.linkedin-page":"Página do LinkedIn","posts.platforms.x":"X","posts.platforms.tiktok":"TikTok","posts.platforms.youtube":"YouTube Shorts","posts.platforms.facebook":"Página do Facebook","posts.platforms.instagram":"Instagram","posts.platforms.threads":"Threads","posts.platforms.pinterest":"Pinterest","posts.platforms.bluesky":"Bluesky","posts.platforms.mastodon":"Mastodon","posts.flash.scheduled":"Post agendado com sucesso!","posts.flash.publishing":"Post está sendo publicado! Pode levar alguns minutos para processar e aparecer em cada plataforma.","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.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_title":"Prévia","posts.create.steps.preview_loading":"Gerando seu conteúdo…","posts.create.steps.preview_error":"Algo deu errado. Por favor, tente novamente.","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.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.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/accounts.php b/lang/pt-BR/accounts.php index 74b03f45..1b20f80c 100644 --- a/lang/pt-BR/accounts.php +++ b/lang/pt-BR/accounts.php @@ -115,4 +115,27 @@ 'already_connected' => 'Esta plataforma já está conectada.', 'no_youtube_channels' => 'Nenhum canal do YouTube encontrado. Por favor, crie um canal primeiro.', ], + + 'popup_callback' => [ + 'title_success' => 'Conectado', + 'title_error' => 'Erro', + 'closing' => 'Esta janela será fechada automaticamente...', + 'close_now' => 'Você pode fechar esta janela agora.', + 'connected' => 'Conta conectada!', + 'reconnected' => 'Conta reconectada!', + 'error_connecting' => 'Erro ao conectar conta. Por favor, tente novamente.', + 'error_connecting_page' => 'Erro ao conectar página. Por favor, tente novamente.', + 'error_connecting_channel' => 'Erro ao conectar canal. Por favor, tente novamente.', + 'session_expired' => 'Sessão expirada. Por favor, tente novamente.', + 'workspace_not_found' => 'Workspace não encontrado.', + 'invalid_state' => 'Estado inválido. Por favor, tente novamente.', + 'failed_to_authenticate' => 'Falha na autenticação.', + 'failed_to_get_profile' => 'Falha ao obter perfil.', + 'page_not_found' => 'Página não encontrada.', + 'channel_not_found' => 'Canal não encontrado.', + 'no_facebook_pages' => 'Nenhuma página do Facebook encontrada. Você precisa ser administrador de pelo menos uma página.', + 'no_facebook_instagram_pages' => 'Nenhuma página do Facebook com conta do Instagram vinculada foi encontrada.', + 'no_youtube_channels' => 'Nenhum canal do YouTube encontrado. Por favor, crie um canal primeiro.', + 'not_linkedin_admin' => 'Você não é administrador de nenhuma página do LinkedIn.', + ], ]; diff --git a/resources/views/auth/social-callback.blade.php b/resources/views/auth/social-callback.blade.php index 13bdb753..f7f131dd 100644 --- a/resources/views/auth/social-callback.blade.php +++ b/resources/views/auth/social-callback.blade.php @@ -1,7 +1,7 @@ - {{ $success ? 'Connected' : 'Error' }} + {{ $success ? __('accounts.popup_callback.title_success') : __('accounts.popup_callback.title_error') }}