diff --git a/app/Actions/Post/UpdatePost.php b/app/Actions/Post/UpdatePost.php index a8212512..acff5899 100644 --- a/app/Actions/Post/UpdatePost.php +++ b/app/Actions/Post/UpdatePost.php @@ -4,21 +4,23 @@ namespace App\Actions\Post; +use App\Enums\Post\Action as PostAction; use App\Enums\Post\Status as PostStatus; use App\Jobs\PublishPost; use App\Models\Post; use App\Models\Workspace; use Carbon\Carbon; +use Illuminate\Support\Arr; class UpdatePost { /** - * @return array{post: Post, action: string|null} + * @return array{post: Post, action: PostAction|null} */ public static function execute(Workspace $workspace, Post $post, array $data): array { if ($post->status === PostStatus::Published) { - return ['post' => $post, 'action' => 'already_published']; + return ['post' => $post, 'action' => PostAction::AlreadyPublished]; } $scheduledAt = $post->scheduled_at; @@ -29,12 +31,12 @@ public static function execute(Workspace $workspace, Post $post, array $data): a $status = data_get($data, 'status', $post->status); $post->update([ - 'status' => $status === 'publishing' ? PostStatus::Publishing : $status, + 'status' => $status === PostStatus::Publishing->value ? PostStatus::Publishing : $status, 'synced' => data_get($data, 'synced', $post->synced), 'scheduled_at' => $scheduledAt, ]); - if (array_key_exists('label_ids', $data)) { + if (Arr::has($data, 'label_ids')) { $post->labels()->sync(data_get($data, 'label_ids', [])); } @@ -52,7 +54,10 @@ public static function execute(Workspace $workspace, Post $post, array $data): a if (data_get($platformData, 'meta') !== null) { $postPlatform = $post->postPlatforms()->where('id', data_get($platformData, 'id'))->first(); - $updateData['meta'] = array_merge($postPlatform->meta ?? [], data_get($platformData, 'meta')); + + if ($postPlatform) { + $updateData['meta'] = array_merge($postPlatform->meta ?? [], data_get($platformData, 'meta')); + } } $post->postPlatforms() @@ -60,15 +65,15 @@ public static function execute(Workspace $workspace, Post $post, array $data): a ->update($updateData); } - if ($status === 'publishing') { + if ($status === PostStatus::Publishing->value) { $post->update(['scheduled_at' => now()]); PublishPost::dispatch($post); - return ['post' => $post, 'action' => 'publishing']; + return ['post' => $post, 'action' => PostAction::Publishing]; } - if ($status === 'scheduled') { - return ['post' => $post, 'action' => 'scheduled']; + if ($status === PostStatus::Scheduled->value) { + return ['post' => $post, 'action' => PostAction::Scheduled]; } return ['post' => $post, 'action' => null]; diff --git a/app/Actions/User/CreateUser.php b/app/Actions/User/CreateUser.php index 21b3b3c8..988434a4 100644 --- a/app/Actions/User/CreateUser.php +++ b/app/Actions/User/CreateUser.php @@ -5,6 +5,7 @@ namespace App\Actions\User; use App\Enums\User\Setup; +use App\Enums\UserWorkspace\Role; use App\Models\User; use App\Models\Workspace; use Illuminate\Support\Facades\DB; @@ -33,7 +34,7 @@ public static function execute(array $data): User 'timezone' => data_get($data, 'timezone', 'UTC'), ]); - $workspace->members()->attach($user->id, ['role' => 'owner']); + $workspace->members()->attach($user->id, ['role' => Role::Owner->value]); $user->update(['current_workspace_id' => $workspace->id]); diff --git a/app/Actions/Workspace/CreateWorkspace.php b/app/Actions/Workspace/CreateWorkspace.php index 37260a1b..1bf6f440 100644 --- a/app/Actions/Workspace/CreateWorkspace.php +++ b/app/Actions/Workspace/CreateWorkspace.php @@ -4,6 +4,7 @@ namespace App\Actions\Workspace; +use App\Enums\UserWorkspace\Role; use App\Models\User; use App\Models\Workspace; @@ -17,7 +18,7 @@ public static function execute(User $user, array $data): Workspace 'timezone' => config('app.timezone', 'UTC'), ]); - $workspace->members()->attach($user->id, ['role' => 'owner']); + $workspace->members()->attach($user->id, ['role' => Role::Owner->value]); $user->switchWorkspace($workspace); if ($user->hasActiveSubscription()) { diff --git a/app/Enums/ApiToken/Status.php b/app/Enums/ApiToken/Status.php new file mode 100644 index 00000000..8c2f96e9 --- /dev/null +++ b/app/Enums/ApiToken/Status.php @@ -0,0 +1,11 @@ +workspace, $post, $validated); - if (data_get($result, 'action') === 'already_published') { + if (data_get($result, 'action') === PostAction::AlreadyPublished) { return response()->json( ['message' => 'Cannot edit a published post.'], Response::HTTP_UNPROCESSABLE_ENTITY diff --git a/app/Http/Controllers/App/ApiKeyController.php b/app/Http/Controllers/App/ApiKeyController.php index 397b6d88..7cb497fb 100644 --- a/app/Http/Controllers/App/ApiKeyController.php +++ b/app/Http/Controllers/App/ApiKeyController.php @@ -22,7 +22,7 @@ public function index(Request $request): Response|RedirectResponse return redirect()->route('app.workspaces.create'); } - $this->authorize('view', $workspace); + $this->authorize('manageTeam', $workspace); return Inertia::render('settings/ApiKeys', [ 'workspace' => $workspace, @@ -38,7 +38,7 @@ public function store(Request $request): RedirectResponse return redirect()->route('app.workspaces.create'); } - $this->authorize('view', $workspace); + $this->authorize('manageTeam', $workspace); $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], @@ -62,7 +62,7 @@ public function destroy(Request $request, ApiToken $apiToken): RedirectResponse return redirect()->route('app.workspaces.create'); } - $this->authorize('view', $workspace); + $this->authorize('manageTeam', $workspace); if ($apiToken->workspace_id !== $workspace->id) { abort(404); diff --git a/app/Http/Controllers/App/BillingController.php b/app/Http/Controllers/App/BillingController.php index 21e87b37..17520fd3 100644 --- a/app/Http/Controllers/App/BillingController.php +++ b/app/Http/Controllers/App/BillingController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\App; +use App\Models\User; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Inertia\Inertia; @@ -16,7 +17,9 @@ public function subscribe(Request $request): Response|RedirectResponse { $user = $request->user(); - if ($user->subscribed('default')) { + $this->authorizeBilling($request); + + if ($user->subscribed(User::SUBSCRIPTION_NAME)) { return redirect()->route('app.billing.index'); } @@ -25,13 +28,15 @@ public function subscribe(Request $request): Response|RedirectResponse ]); } - public function index(Request $request): Response + public function index(Request $request): Response|RedirectResponse { + $this->authorizeBilling($request); + $user = $request->user(); - $subscription = $user->subscription('default'); + $subscription = $user->subscription(User::SUBSCRIPTION_NAME); return Inertia::render('billing/Index', [ - 'hasSubscription' => $user->subscribed('default'), + 'hasSubscription' => $user->subscribed(User::SUBSCRIPTION_NAME), 'onTrial' => $subscription?->onTrial() ?? false, 'trialEndsAt' => $subscription?->trial_ends_at?->toFormattedDateString(), 'subscription' => $subscription?->only([ @@ -58,9 +63,11 @@ public function index(Request $request): Response public function checkout(Request $request): SymfonyResponse { + $this->authorizeBilling($request); + $user = $request->user(); - $subscription = $user->newSubscription('default', config('cashier.plans.monthly.price_id')) + $subscription = $user->newSubscription(User::SUBSCRIPTION_NAME, config('cashier.plans.monthly.price_id')) ->allowPromotionCodes() ->trialDays(config('cashier.trial_days')) ->quantity(1); @@ -78,7 +85,7 @@ public function processing(Request $request): Response|RedirectResponse $user = $request->user(); $status = $request->query('status', 'processing'); - if ($user->subscribed('default')) { + if ($user->subscribed(User::SUBSCRIPTION_NAME)) { return redirect()->route('app.calendar'); } @@ -94,8 +101,19 @@ public function processing(Request $request): Response|RedirectResponse public function portal(Request $request): RedirectResponse { + $this->authorizeBilling($request); + return $request->user()->redirectToBillingPortal( route('app.billing.index') ); } + + private function authorizeBilling(Request $request): void + { + $workspace = $request->user()->currentWorkspace; + + if ($workspace) { + $this->authorize('manageBilling', $workspace); + } + } } diff --git a/app/Http/Controllers/App/MediaController.php b/app/Http/Controllers/App/MediaController.php index f39ae4ee..7e587cd4 100644 --- a/app/Http/Controllers/App/MediaController.php +++ b/app/Http/Controllers/App/MediaController.php @@ -120,9 +120,9 @@ public function duplicate(Media $media, Request $request): JsonResponse $duplicates = []; foreach ($targets as $target) { - $model = $this->resolveModel($target['model'], $target['model_id']); + $model = $this->resolveModel(data_get($target, 'model'), data_get($target, 'model_id')); $this->authorizeModelOwnership($model, $request); - $collection = $target['collection'] ?? $media->collection; + $collection = data_get($target, 'collection', $media->collection); $duplicate = $model->media()->create([ 'group_id' => $media->group_id, diff --git a/app/Http/Controllers/App/NotificationController.php b/app/Http/Controllers/App/NotificationController.php index bae8459a..5c5cb00a 100644 --- a/app/Http/Controllers/App/NotificationController.php +++ b/app/Http/Controllers/App/NotificationController.php @@ -15,6 +15,10 @@ public function index(Request $request): JsonResponse { $workspace = $request->user()->currentWorkspace; + if (! $workspace) { + return response()->json(['notifications' => [], 'unread_count' => 0]); + } + $notifications = $request->user() ->notifications() ->where('workspace_id', $workspace->id) @@ -51,6 +55,10 @@ public function markAllAsRead(Request $request): JsonResponse { $workspace = $request->user()->currentWorkspace; + if (! $workspace) { + return response()->json(['success' => true]); + } + $request->user() ->notifications() ->where('workspace_id', $workspace->id) @@ -64,6 +72,10 @@ public function archiveAll(Request $request): JsonResponse { $workspace = $request->user()->currentWorkspace; + if (! $workspace) { + return response()->json(['success' => true]); + } + $request->user() ->notifications() ->where('workspace_id', $workspace->id) diff --git a/app/Http/Controllers/App/OnboardingController.php b/app/Http/Controllers/App/OnboardingController.php index b6a03e30..29f619eb 100644 --- a/app/Http/Controllers/App/OnboardingController.php +++ b/app/Http/Controllers/App/OnboardingController.php @@ -36,7 +36,7 @@ public function storeRole(Request $request): RedirectResponse ]); $request->user()->update([ - 'persona' => $validated['persona'], + 'persona' => data_get($validated, 'persona'), 'setup' => Setup::Connections, ]); @@ -92,7 +92,7 @@ public function storeConnect(Request $request): SymfonyResponse|RedirectResponse 'setup' => Setup::Subscription, ]); - $subscription = $user->newSubscription('default', config('cashier.plans.monthly.price_id')) + $subscription = $user->newSubscription(User::SUBSCRIPTION_NAME, config('cashier.plans.monthly.price_id')) ->allowPromotionCodes() ->trialDays(config('cashier.trial_days')) ->quantity(1); diff --git a/app/Http/Controllers/App/PostController.php b/app/Http/Controllers/App/PostController.php index 6a129b90..08606d8d 100644 --- a/app/Http/Controllers/App/PostController.php +++ b/app/Http/Controllers/App/PostController.php @@ -7,6 +7,8 @@ use App\Actions\Post\CreatePost; use App\Actions\Post\DeletePost; use App\Actions\Post\UpdatePost; +use App\Enums\Post\Action as PostAction; +use App\Enums\Post\Status as PostStatus; use App\Enums\SocialAccount\Platform; use App\Http\Requests\App\Post\UpdatePostRequest; use App\Models\Post; @@ -34,9 +36,9 @@ public function index(Request $request, ?string $status = null): Response|Redire if ($status) { $query = match ($status) { - 'draft' => $query->draft(), - 'scheduled' => $query->scheduled(), - 'published' => $query->published(), + PostStatus::Draft->value => $query->draft(), + PostStatus::Scheduled->value => $query->scheduled(), + PostStatus::Published->value => $query->published(), default => $query, }; } @@ -204,21 +206,21 @@ public function update(UpdatePostRequest $request, Post $post): RedirectResponse $action = data_get($result, 'action'); - if ($action === 'already_published') { + if ($action === PostAction::AlreadyPublished) { session()->flash('flash.banner', __('posts.flash.cannot_edit_published')); session()->flash('flash.bannerStyle', 'danger'); return back(); } - if ($action === 'publishing') { + if ($action === PostAction::Publishing) { session()->flash('flash.banner', __('posts.flash.publishing')); session()->flash('flash.bannerStyle', 'success'); return redirect()->route('app.posts.edit', $post); } - if ($action === 'scheduled') { + if ($action === PostAction::Scheduled) { session()->flash('flash.banner', __('posts.flash.scheduled')); session()->flash('flash.bannerStyle', 'success'); diff --git a/app/Http/Controllers/App/Settings/PasswordController.php b/app/Http/Controllers/App/Settings/PasswordController.php index afbb14a7..416393d0 100644 --- a/app/Http/Controllers/App/Settings/PasswordController.php +++ b/app/Http/Controllers/App/Settings/PasswordController.php @@ -27,7 +27,7 @@ public function update(Request $request): RedirectResponse ]); $request->user()->update([ - 'password' => Hash::make($validated['password']), + 'password' => Hash::make(data_get($validated, 'password')), ]); session()->flash('flash.banner', __('settings.flash.password_updated')); diff --git a/app/Http/Controllers/App/Settings/ProfileController.php b/app/Http/Controllers/App/Settings/ProfileController.php index ef0cd314..58d62630 100644 --- a/app/Http/Controllers/App/Settings/ProfileController.php +++ b/app/Http/Controllers/App/Settings/ProfileController.php @@ -8,6 +8,7 @@ use App\Http\Controllers\Controller; use App\Http\Requests\App\Settings\ProfileDeleteRequest; use App\Http\Requests\App\Settings\ProfileUpdateRequest; +use App\Models\User; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -90,8 +91,8 @@ public function destroy(ProfileDeleteRequest $request): RedirectResponse $user = $request->user(); DB::transaction(function () use ($user) { - if ($user->subscribed('default')) { - $user->subscription('default')->cancelNow(); + if ($user->subscribed(User::SUBSCRIPTION_NAME)) { + $user->subscription(User::SUBSCRIPTION_NAME)->cancelNow(); } $user->subscriptions()->delete(); $user->update(['current_workspace_id' => null]); diff --git a/app/Http/Controllers/App/WorkspaceController.php b/app/Http/Controllers/App/WorkspaceController.php index 0e3cebf8..a4c43721 100644 --- a/app/Http/Controllers/App/WorkspaceController.php +++ b/app/Http/Controllers/App/WorkspaceController.php @@ -6,6 +6,7 @@ use App\Actions\Workspace\CreateWorkspace; use App\Actions\Workspace\DeleteWorkspace; +use App\Enums\UserWorkspace\Role; use App\Http\Requests\App\Workspace\StoreWorkspaceRequest; use App\Http\Requests\App\Workspace\UpdateWorkspaceRequest; use App\Models\Workspace; @@ -94,7 +95,7 @@ public function settings(Request $request): Response|RedirectResponse 'name' => $member->name, 'email' => $member->email, 'role' => $member->pivot->role, - 'is_owner' => $member->id === $workspace->user_id, + 'is_owner' => $member->pivot->role === Role::Owner->value, ]); $invitations = $workspace->invites() diff --git a/app/Http/Controllers/App/WorkspaceHashtagController.php b/app/Http/Controllers/App/WorkspaceHashtagController.php index 44bd230a..3666c097 100644 --- a/app/Http/Controllers/App/WorkspaceHashtagController.php +++ b/app/Http/Controllers/App/WorkspaceHashtagController.php @@ -23,7 +23,7 @@ public function index(Request $request): Response|RedirectResponse return redirect()->route('app.workspaces.create'); } - $this->authorize('view', $workspace); + $this->authorize('createPost', $workspace); $hashtags = $workspace->hashtags() ->when($request->input('search'), fn ($query, $search) => $query->where('name', 'ilike', "%{$search}%")) @@ -47,7 +47,7 @@ public function store(Request $request): RedirectResponse return redirect()->route('app.workspaces.create'); } - $this->authorize('view', $workspace); + $this->authorize('createPost', $workspace); $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], @@ -70,7 +70,7 @@ public function update(Request $request, WorkspaceHashtag $hashtag): RedirectRes return redirect()->route('app.workspaces.create'); } - $this->authorize('view', $workspace); + $this->authorize('createPost', $workspace); if ($hashtag->workspace_id !== $workspace->id) { abort(404); @@ -97,7 +97,7 @@ public function destroy(Request $request, WorkspaceHashtag $hashtag): RedirectRe return redirect()->route('app.workspaces.create'); } - $this->authorize('view', $workspace); + $this->authorize('createPost', $workspace); if ($hashtag->workspace_id !== $workspace->id) { abort(404); diff --git a/app/Http/Controllers/App/WorkspaceInviteController.php b/app/Http/Controllers/App/WorkspaceInviteController.php index 763301df..5931d5b2 100644 --- a/app/Http/Controllers/App/WorkspaceInviteController.php +++ b/app/Http/Controllers/App/WorkspaceInviteController.php @@ -34,7 +34,7 @@ public function index(Request $request): Response|RedirectResponse ->latest() ->get(), 'members' => $workspace->members() - ->where('user_id', '!=', $workspace->user_id) + ->wherePivot('role', '!=', WorkspaceRole::Owner->value) ->get() ->map(fn ($member) => [ 'id' => $member->id, @@ -123,7 +123,9 @@ public function removeMember(Request $request, string $userId): RedirectResponse $this->authorize('manageTeam', $workspace); - if ($workspace->user_id === $userId) { + $memberPivot = $workspace->members()->where('user_id', $userId)->first()?->pivot; + + if ($memberPivot && $memberPivot->role === WorkspaceRole::Owner->value) { return back()->withErrors(['member' => 'Cannot remove the workspace owner.']); } @@ -145,7 +147,9 @@ public function updateRole(Request $request, string $userId): RedirectResponse $this->authorize('manageTeam', $workspace); - if ($workspace->user_id === $userId) { + $memberPivot = $workspace->members()->where('user_id', $userId)->first()?->pivot; + + if ($memberPivot && $memberPivot->role === WorkspaceRole::Owner->value) { return back()->withErrors(['role' => 'Cannot change the workspace owner role.']); } diff --git a/app/Http/Controllers/App/WorkspaceLabelController.php b/app/Http/Controllers/App/WorkspaceLabelController.php index 3bcb4943..bd904c22 100644 --- a/app/Http/Controllers/App/WorkspaceLabelController.php +++ b/app/Http/Controllers/App/WorkspaceLabelController.php @@ -23,7 +23,7 @@ public function index(Request $request): Response|RedirectResponse return redirect()->route('app.workspaces.create'); } - $this->authorize('view', $workspace); + $this->authorize('createPost', $workspace); $labels = $workspace->labels() ->when($request->input('search'), fn ($query, $search) => $query->where('name', 'ilike', "%{$search}%")) @@ -47,7 +47,7 @@ public function store(Request $request): RedirectResponse return redirect()->route('app.workspaces.create'); } - $this->authorize('view', $workspace); + $this->authorize('createPost', $workspace); $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], @@ -70,7 +70,7 @@ public function update(Request $request, WorkspaceLabel $label): RedirectRespons return redirect()->route('app.workspaces.create'); } - $this->authorize('view', $workspace); + $this->authorize('createPost', $workspace); if ($label->workspace_id !== $workspace->id) { abort(404); @@ -97,7 +97,7 @@ public function destroy(Request $request, WorkspaceLabel $label): RedirectRespon return redirect()->route('app.workspaces.create'); } - $this->authorize('view', $workspace); + $this->authorize('createPost', $workspace); if ($label->workspace_id !== $workspace->id) { abort(404); diff --git a/app/Http/Controllers/Auth/BlueskyController.php b/app/Http/Controllers/Auth/BlueskyController.php index 31326248..13b1b8d2 100644 --- a/app/Http/Controllers/Auth/BlueskyController.php +++ b/app/Http/Controllers/Auth/BlueskyController.php @@ -79,9 +79,9 @@ public function store(Request $request): View|RedirectResponse $data = $response->json(); // Get profile - $profileResponse = Http::withToken($data['accessJwt']) + $profileResponse = Http::withToken(data_get($data, 'accessJwt')) ->get("{$service}/xrpc/app.bsky.actor.getProfile", [ - 'actor' => $data['did'], + 'actor' => data_get($data, 'did'), ]); $profile = $profileResponse->successful() ? $profileResponse->json() : []; @@ -95,16 +95,16 @@ public function store(Request $request): View|RedirectResponse return back()->withErrors(['identifier' => 'Bluesky is already connected.']); } - $avatarPath = isset($profile['avatar']) ? uploadFromUrl($profile['avatar']) : null; + $avatarPath = data_get($profile, 'avatar') ? uploadFromUrl(data_get($profile, 'avatar')) : null; $accountData = [ 'platform' => $this->platform->value, - 'platform_user_id' => $data['did'], - 'username' => $data['handle'], - 'display_name' => $profile['displayName'] ?? $data['handle'], + 'platform_user_id' => data_get($data, 'did'), + 'username' => data_get($data, 'handle'), + 'display_name' => data_get($profile, 'displayName', data_get($data, 'handle')), 'avatar_url' => $avatarPath, - 'access_token' => $data['accessJwt'], - 'refresh_token' => $data['refreshJwt'], + 'access_token' => data_get($data, 'accessJwt'), + 'refresh_token' => data_get($data, 'refreshJwt'), 'token_expires_at' => now()->addHours(2), 'meta' => [ 'service' => $service, diff --git a/app/Http/Controllers/Auth/FacebookController.php b/app/Http/Controllers/Auth/FacebookController.php index f89aea95..06af7f1c 100644 --- a/app/Http/Controllers/Auth/FacebookController.php +++ b/app/Http/Controllers/Auth/FacebookController.php @@ -101,21 +101,21 @@ public function callback(Request $request): View|RedirectResponse // If only one page, connect directly if (count($pages) === 1) { $page = $pages[0]; - $avatarPath = uploadFromUrl($page['picture']); + $avatarPath = uploadFromUrl(data_get($page, 'picture')); if ($existingAccount) { // Reconnect existing account $existingAccount->update([ - 'platform_user_id' => $page['id'], - 'username' => $page['username'] ?? null, - 'display_name' => $page['name'], + 'platform_user_id' => data_get($page, 'id'), + 'username' => data_get($page, 'username', null), + 'display_name' => data_get($page, 'name'), 'avatar_url' => $avatarPath, - 'access_token' => $page['access_token'], + 'access_token' => data_get($page, 'access_token'), 'refresh_token' => null, 'token_expires_at' => null, 'scopes' => $this->scopes, 'meta' => [ - 'page_id' => $page['id'], + 'page_id' => data_get($page, 'id'), 'user_id' => $socialUser->getId(), 'user_token' => $socialUser->token, ], @@ -130,17 +130,17 @@ public function callback(Request $request): View|RedirectResponse // Create new account $workspace->socialAccounts()->create([ 'platform' => $this->platform->value, - 'platform_user_id' => $page['id'], - 'username' => $page['username'] ?? null, - 'display_name' => $page['name'], + 'platform_user_id' => data_get($page, 'id'), + 'username' => data_get($page, 'username', null), + 'display_name' => data_get($page, 'name'), 'avatar_url' => $avatarPath, - 'access_token' => $page['access_token'], + 'access_token' => data_get($page, 'access_token'), 'refresh_token' => null, // Page tokens don't expire if user token is long-lived 'token_expires_at' => null, 'scopes' => $this->scopes, 'status' => Status::Connected, 'meta' => [ - 'page_id' => $page['id'], + 'page_id' => data_get($page, 'id'), 'user_id' => $socialUser->getId(), 'user_token' => $socialUser->token, ], @@ -195,7 +195,7 @@ public function selectPage(Request $request) return Inertia::render('accounts/FacebookPageSelect', [ 'workspace' => $workspace, - 'pages' => $oauthData['pages'], + 'pages' => data_get($oauthData, 'pages'), ]); } @@ -219,14 +219,14 @@ public function select(Request $request): View } try { - $selectedPage = collect($oauthData['pages'])->firstWhere('id', $request->page_id); + $selectedPage = collect(data_get($oauthData, 'pages'))->firstWhere('id', $request->page_id); if (! $selectedPage) { return $this->popupCallback(false, 'Page not found.', $this->platform->value); } - $avatarPath = uploadFromUrl($selectedPage['picture']); - $reconnectId = $oauthData['reconnect_id'] ?? null; + $avatarPath = uploadFromUrl(data_get($selectedPage, 'picture')); + $reconnectId = data_get($oauthData, 'reconnect_id'); if ($reconnectId) { // Reconnect existing account @@ -234,18 +234,18 @@ public function select(Request $request): View if ($existingAccount) { $existingAccount->update([ - 'platform_user_id' => $selectedPage['id'], - 'username' => $selectedPage['username'] ?? null, - 'display_name' => $selectedPage['name'], + 'platform_user_id' => data_get($selectedPage, 'id'), + 'username' => data_get($selectedPage, 'username') ?? null, + 'display_name' => data_get($selectedPage, 'name'), 'avatar_url' => $avatarPath, - 'access_token' => $selectedPage['access_token'], + 'access_token' => data_get($selectedPage, 'access_token'), 'refresh_token' => null, 'token_expires_at' => null, 'scopes' => $this->scopes, 'meta' => [ - 'page_id' => $selectedPage['id'], - 'user_id' => $oauthData['user_id'], - 'user_token' => $oauthData['user_token'], + 'page_id' => data_get($selectedPage, 'id'), + 'user_id' => data_get($oauthData, 'user_id'), + 'user_token' => data_get($oauthData, 'user_token'), ], ]); $existingAccount->markAsConnected(); @@ -259,19 +259,19 @@ public function select(Request $request): View // Create new account $workspace->socialAccounts()->create([ 'platform' => $this->platform->value, - 'platform_user_id' => $selectedPage['id'], - 'username' => $selectedPage['username'] ?? null, - 'display_name' => $selectedPage['name'], + 'platform_user_id' => data_get($selectedPage, 'id'), + 'username' => data_get($selectedPage, 'username') ?? null, + 'display_name' => data_get($selectedPage, 'name'), 'avatar_url' => $avatarPath, - 'access_token' => $selectedPage['access_token'], + 'access_token' => data_get($selectedPage, 'access_token'), 'refresh_token' => null, 'token_expires_at' => null, 'scopes' => $this->scopes, 'status' => Status::Connected, 'meta' => [ - 'page_id' => $selectedPage['id'], - 'user_id' => $oauthData['user_id'], - 'user_token' => $oauthData['user_token'], + 'page_id' => data_get($selectedPage, 'id'), + 'user_id' => data_get($oauthData, 'user_id'), + 'user_token' => data_get($oauthData, 'user_token'), ], ]); @@ -306,12 +306,12 @@ private function fetchPages(string $userToken): array $data = $response->json(); - return collect($data['data'] ?? [])->map(fn ($page) => [ - 'id' => $page['id'], - 'name' => $page['name'], - 'username' => $page['username'] ?? null, - 'picture' => $page['picture']['data']['url'] ?? null, - 'access_token' => $page['access_token'], + return collect(data_get($data, 'data', []))->map(fn ($page) => [ + 'id' => data_get($page, 'id'), + 'name' => data_get($page, 'name'), + 'username' => data_get($page, 'username', null), + 'picture' => data_get($page, 'picture.data.url'), + 'access_token' => data_get($page, 'access_token'), ])->toArray(); } catch (\Exception $e) { Log::error('Facebook pages fetch error', [ diff --git a/app/Http/Controllers/Auth/LinkedInPageController.php b/app/Http/Controllers/Auth/LinkedInPageController.php index 99567702..d983cc47 100644 --- a/app/Http/Controllers/Auth/LinkedInPageController.php +++ b/app/Http/Controllers/Auth/LinkedInPageController.php @@ -262,18 +262,16 @@ private function fetchOrganizations(string $accessToken): array $data = $response->json(); $organizations = []; - foreach ($data['elements'] ?? [] as $element) { - $org = $element['organization~'] ?? null; + foreach (data_get($data, 'elements', []) as $element) { + $org = data_get($element, 'organization~', null); if ($org) { $logoUrl = null; - if (isset($org['logoV2']['original~']['elements'][0]['identifiers'][0]['identifier'])) { - $logoUrl = $org['logoV2']['original~']['elements'][0]['identifiers'][0]['identifier']; - } + $logoUrl = data_get($org, 'logoV2.original~.elements.0.identifiers.0.identifier'); $organizations[] = [ - 'id' => $org['id'], - 'name' => $org['localizedName'] ?? 'Unknown', - 'vanity_name' => $org['vanityName'] ?? null, + 'id' => data_get($org, 'id'), + 'name' => data_get($org, 'localizedName', 'Unknown'), + 'vanity_name' => data_get($org, 'vanityName', null), 'logo' => $logoUrl, ]; } diff --git a/app/Http/Controllers/Auth/SocialController.php b/app/Http/Controllers/Auth/SocialController.php index c8d7c564..40469876 100644 --- a/app/Http/Controllers/Auth/SocialController.php +++ b/app/Http/Controllers/Auth/SocialController.php @@ -85,6 +85,10 @@ public function toggleActive(Request $request, SocialAccount $account): Redirect { $workspace = $request->user()->currentWorkspace; + if (! $workspace) { + return redirect()->route('app.workspaces.create'); + } + $this->authorize('manageAccounts', $workspace); if ($account->workspace_id !== $workspace->id) { diff --git a/app/Http/Controllers/Auth/SocialLoginController.php b/app/Http/Controllers/Auth/SocialLoginController.php index 3ef73a58..40c552d9 100644 --- a/app/Http/Controllers/Auth/SocialLoginController.php +++ b/app/Http/Controllers/Auth/SocialLoginController.php @@ -21,7 +21,11 @@ public function redirect(): RedirectResponse public function callback(): RedirectResponse { - $googleUser = Socialite::driver('google-auth')->user(); + try { + $googleUser = Socialite::driver('google-auth')->user(); + } catch (\Exception) { + return redirect()->route('login'); + } $user = User::where('email', $googleUser->getEmail())->first(); diff --git a/app/Http/Controllers/Auth/YouTubeController.php b/app/Http/Controllers/Auth/YouTubeController.php index a401130c..b6925a1e 100644 --- a/app/Http/Controllers/Auth/YouTubeController.php +++ b/app/Http/Controllers/Auth/YouTubeController.php @@ -95,21 +95,21 @@ public function callback(Request $request): View|RedirectResponse // If only one channel, connect directly (most common case) if (count($channels) === 1) { $channel = $channels[0]; - $avatarPath = uploadFromUrl($channel['thumbnail']); + $avatarPath = uploadFromUrl(data_get($channel, 'thumbnail')); if ($existingAccount) { // Reconnect existing account $existingAccount->update([ - 'platform_user_id' => $channel['id'], - 'username' => ltrim($channel['custom_url'] ?? $channel['id'], '@'), - 'display_name' => $channel['title'], + 'platform_user_id' => data_get($channel, 'id'), + 'username' => ltrim(data_get($channel, 'custom_url', data_get($channel, 'id')), '@'), + 'display_name' => data_get($channel, 'title'), 'avatar_url' => $avatarPath, 'access_token' => $socialUser->token, 'refresh_token' => $socialUser->refreshToken, 'token_expires_at' => $socialUser->expiresIn ? now()->addSeconds($socialUser->expiresIn) : null, 'scopes' => $this->scopes, 'meta' => [ - 'channel_id' => $channel['id'], + 'channel_id' => data_get($channel, 'id'), 'google_user_id' => $socialUser->getId(), ], ]); @@ -123,9 +123,9 @@ public function callback(Request $request): View|RedirectResponse // Create new account $workspace->socialAccounts()->create([ 'platform' => $this->platform->value, - 'platform_user_id' => $channel['id'], - 'username' => ltrim($channel['custom_url'] ?? $channel['id'], '@'), - 'display_name' => $channel['title'], + 'platform_user_id' => data_get($channel, 'id'), + 'username' => ltrim(data_get($channel, 'custom_url', data_get($channel, 'id')), '@'), + 'display_name' => data_get($channel, 'title'), 'avatar_url' => $avatarPath, 'access_token' => $socialUser->token, 'refresh_token' => $socialUser->refreshToken, @@ -133,7 +133,7 @@ public function callback(Request $request): View|RedirectResponse 'scopes' => $this->scopes, 'status' => Status::Connected, 'meta' => [ - 'channel_id' => $channel['id'], + 'channel_id' => data_get($channel, 'id'), 'google_user_id' => $socialUser->getId(), ], ]); @@ -328,13 +328,13 @@ private function fetchChannels(string $accessToken): array $data = $response->json(); - return collect($data['items'] ?? [])->map(fn ($channel) => [ - 'id' => $channel['id'], - 'title' => $channel['snippet']['title'], - 'description' => $channel['snippet']['description'] ?? '', - 'thumbnail' => $channel['snippet']['thumbnails']['default']['url'] ?? null, - 'custom_url' => $channel['snippet']['customUrl'] ?? null, - 'subscriber_count' => $channel['statistics']['subscriberCount'] ?? 0, + return collect(data_get($data, 'items', []))->map(fn ($channel) => [ + 'id' => data_get($channel, 'id'), + 'title' => data_get($channel, 'snippet.title'), + 'description' => data_get($channel, 'snippet.description', ''), + 'thumbnail' => data_get($channel, 'snippet.thumbnails.default.url'), + 'custom_url' => data_get($channel, 'snippet.customUrl'), + 'subscriber_count' => data_get($channel, 'statistics.subscriberCount', 0), ])->toArray(); } catch (\Exception $e) { Log::error('YouTube channels fetch error', [ diff --git a/app/Http/Middleware/Api/AuthenticateApiToken.php b/app/Http/Middleware/Api/AuthenticateApiToken.php index 6acac27a..0f5f71a4 100644 --- a/app/Http/Middleware/Api/AuthenticateApiToken.php +++ b/app/Http/Middleware/Api/AuthenticateApiToken.php @@ -5,6 +5,7 @@ namespace App\Http\Middleware\Api; use App\Models\ApiToken; +use App\Models\User; use App\Models\Workspace; use Closure; use Illuminate\Http\Request; @@ -60,6 +61,6 @@ private function hasActiveSubscription(Workspace $workspace): bool return false; } - return $owner->subscribed('default') || $owner->onTrial('default'); + return $owner->subscribed(User::SUBSCRIPTION_NAME) || $owner->onTrial(User::SUBSCRIPTION_NAME); } } diff --git a/app/Http/Middleware/App/EnsureSubscribed.php b/app/Http/Middleware/App/EnsureSubscribed.php index afbca501..cbe1817f 100644 --- a/app/Http/Middleware/App/EnsureSubscribed.php +++ b/app/Http/Middleware/App/EnsureSubscribed.php @@ -4,6 +4,7 @@ namespace App\Http\Middleware\App; +use App\Models\User; use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; @@ -29,7 +30,7 @@ public function handle(Request $request, Closure $next): Response } // Allow access if user has active subscription or is on trial - if ($user->subscribed('default') || $user->onTrial('default')) { + if ($user->subscribed(User::SUBSCRIPTION_NAME) || $user->onTrial(User::SUBSCRIPTION_NAME)) { return $next($request); } @@ -39,7 +40,7 @@ public function handle(Request $request, Closure $next): Response if ($currentWorkspace && $currentWorkspace->owner && $currentWorkspace->owner->id !== $user->id) { $owner = $currentWorkspace->owner; - if ($owner->subscribed('default') || $owner->onTrial('default')) { + if ($owner->subscribed(User::SUBSCRIPTION_NAME) || $owner->onTrial(User::SUBSCRIPTION_NAME)) { return $next($request); } } diff --git a/app/Http/Middleware/Mcp/AuthenticateMcpToken.php b/app/Http/Middleware/Mcp/AuthenticateMcpToken.php index 3b6195dd..1dee934d 100644 --- a/app/Http/Middleware/Mcp/AuthenticateMcpToken.php +++ b/app/Http/Middleware/Mcp/AuthenticateMcpToken.php @@ -5,6 +5,7 @@ namespace App\Http\Middleware\Mcp; use App\Models\ApiToken; +use App\Models\User; use App\Models\Workspace; use Closure; use Illuminate\Http\Request; @@ -64,6 +65,6 @@ private function hasActiveSubscription(Workspace $workspace): bool return false; } - return $owner->subscribed('default') || $owner->onTrial('default'); + return $owner->subscribed(User::SUBSCRIPTION_NAME) || $owner->onTrial(User::SUBSCRIPTION_NAME); } } diff --git a/app/Jobs/PublishToSocialPlatform.php b/app/Jobs/PublishToSocialPlatform.php index fc3a053a..ba8afdef 100644 --- a/app/Jobs/PublishToSocialPlatform.php +++ b/app/Jobs/PublishToSocialPlatform.php @@ -64,7 +64,7 @@ public function handle(): void $publisher = $this->getPublisher(); $result = $publisher->publish($this->postPlatform); - $this->postPlatform->markAsPublished($result['id'], $result['url'] ?? null); + $this->postPlatform->markAsPublished(data_get($result, 'id'), data_get($result, 'url')); } catch (TokenExpiredException $e) { Log::error('Token expired while publishing to social platform', [ 'post_platform_id' => $this->postPlatform->id, diff --git a/app/Jobs/SendNotification.php b/app/Jobs/SendNotification.php index 81d055b7..46944eed 100644 --- a/app/Jobs/SendNotification.php +++ b/app/Jobs/SendNotification.php @@ -52,7 +52,7 @@ public function handle(): void } // Send email (respects user preferences) - if ($this->mailable && $this->channel !== Channel::InApp && $this->user->wantsEmailFor($this->type->value)) { + if ($this->mailable && $this->channel !== Channel::InApp && $this->user->wantsEmailFor($this->type)) { Mail::to($this->user)->send($this->mailable); } } diff --git a/app/Jobs/SendPostHogEvent.php b/app/Jobs/SendPostHogEvent.php index 47cfdece..59b880dd 100644 --- a/app/Jobs/SendPostHogEvent.php +++ b/app/Jobs/SendPostHogEvent.php @@ -36,11 +36,11 @@ public function handle(): void } foreach ($this->calls as $call) { - match ($call['method']) { - 'capture' => PostHog::capture($call['payload']), - 'identify' => PostHog::identify($call['payload']), - 'groupIdentify' => PostHog::groupIdentify($call['payload']), - default => Log::warning('SendPostHogEvent: unknown method', ['method' => $call['method']]), + 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('SendPostHogEvent: unknown method', ['method' => data_get($call, 'method')]), }; } diff --git a/app/Mcp/Tools/ApiKey/CreateApiKeyTool.php b/app/Mcp/Tools/ApiKey/CreateApiKeyTool.php index 62886253..a421a687 100644 --- a/app/Mcp/Tools/ApiKey/CreateApiKeyTool.php +++ b/app/Mcp/Tools/ApiKey/CreateApiKeyTool.php @@ -25,8 +25,8 @@ public function handle(Request $request): ResponseFactory $result = CreateApiKey::execute($request->user()->currentWorkspace, $validated); return Response::structured([ - ...$result['token']->toArray(), - 'token' => $result['plain_token'], + ...data_get($result, 'token')->toArray(), + 'token' => data_get($result, 'plain_token'), ]); } diff --git a/app/Models/ApiToken.php b/app/Models/ApiToken.php index e5dbacdf..d1838ddb 100644 --- a/app/Models/ApiToken.php +++ b/app/Models/ApiToken.php @@ -4,6 +4,7 @@ namespace App\Models; +use App\Enums\ApiToken\Status; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Concerns\HasVersion4Uuids as HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -60,10 +61,10 @@ protected function status(): Attribute return Attribute::make( get: function () { if ($this->expires_at === null) { - return 'active'; + return Status::Active->value; } - return now()->greaterThan($this->expires_at) ? 'expired' : 'active'; + return now()->greaterThan($this->expires_at) ? Status::Expired->value : Status::Active->value; } ); } diff --git a/app/Models/Traits/HasWorkspace.php b/app/Models/Traits/HasWorkspace.php index 768e636c..451d2689 100644 --- a/app/Models/Traits/HasWorkspace.php +++ b/app/Models/Traits/HasWorkspace.php @@ -4,6 +4,8 @@ namespace App\Models\Traits; +use App\Enums\UserWorkspace\Role; +use App\Models\User; use App\Models\Workspace; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; @@ -49,7 +51,7 @@ public function belongsToWorkspace(Workspace $workspace): bool */ public function ownedWorkspacesCount(): int { - return $this->workspaces()->wherePivot('role', 'owner')->count(); + return $this->workspaces()->wherePivot('role', Role::Owner->value)->count(); } /** @@ -66,7 +68,7 @@ public function canCreateWorkspace(): bool return $this->ownedWorkspacesCount() === 0; } - $subscription = $this->subscription('default'); + $subscription = $this->subscription(User::SUBSCRIPTION_NAME); return $subscription && $this->ownedWorkspacesCount() < $subscription->quantity; } @@ -82,7 +84,7 @@ public function incrementWorkspaceQuantity(): void } if ($this->hasActiveSubscription()) { - $this->subscription('default')->incrementQuantity(); + $this->subscription(User::SUBSCRIPTION_NAME)->incrementQuantity(); } } @@ -97,7 +99,7 @@ public function decrementWorkspaceQuantity(): void } if ($this->hasActiveSubscription()) { - $subscription = $this->subscription('default'); + $subscription = $this->subscription(User::SUBSCRIPTION_NAME); if ($subscription->quantity > 1) { $subscription->decrementQuantity(); @@ -119,7 +121,7 @@ public function syncWorkspaceQuantity(): void $count = $this->ownedWorkspacesCount(); if ($count > 0) { - $this->subscription('default')->updateQuantity($count); + $this->subscription(User::SUBSCRIPTION_NAME)->updateQuantity($count); } } } diff --git a/app/Models/User.php b/app/Models/User.php index 7000226a..4b9c00e5 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,6 +4,7 @@ namespace App\Models; +use App\Enums\Notification\Type as NotificationType; use App\Enums\User\Persona; use App\Enums\User\Setup; use App\Models\Traits\HasMedia; @@ -23,6 +24,8 @@ class User extends Authenticatable implements MustVerifyEmail /** @use HasFactory */ use Billable, HasFactory, HasMedia, HasUuids, HasWorkspace, Notifiable; + public const SUBSCRIPTION_NAME = 'default'; + /** * The attributes that are mass assignable. * @@ -94,7 +97,7 @@ public function notificationPreference(): HasOne return $this->hasOne(NotificationPreference::class); } - public function wantsEmailFor(string $type): bool + public function wantsEmailFor(NotificationType $type): bool { $preference = $this->notificationPreference; @@ -103,9 +106,9 @@ public function wantsEmailFor(string $type): bool } return match ($type) { - 'post_published' => $preference->post_published, - 'post_failed', 'post_partially_published' => $preference->post_failed, - 'account_disconnected' => $preference->account_disconnected, + NotificationType::PostPublished => $preference->post_published, + NotificationType::PostFailed, NotificationType::PostPartiallyPublished => $preference->post_failed, + NotificationType::AccountDisconnected => $preference->account_disconnected, default => true, }; } @@ -120,7 +123,7 @@ public function hasActiveSubscription(): bool return true; } - return $this->subscribed('default'); + return $this->subscribed(self::SUBSCRIPTION_NAME); } /** diff --git a/app/Policies/WorkspacePolicy.php b/app/Policies/WorkspacePolicy.php index 5c0e61ed..89ef7d7d 100644 --- a/app/Policies/WorkspacePolicy.php +++ b/app/Policies/WorkspacePolicy.php @@ -4,7 +4,7 @@ namespace App\Policies; -use App\Enums\UserWorkspace\Role as WorkspaceRole; +use App\Enums\UserWorkspace\Role; use App\Models\User; use App\Models\Workspace; @@ -17,7 +17,7 @@ public function viewAny(User $user): bool public function view(User $user, Workspace $workspace): bool { - return $this->isOwner($user, $workspace) || $workspace->members->contains($user); + return $this->isMember($user, $workspace); } public function create(User $user): bool @@ -27,45 +27,48 @@ public function create(User $user): bool public function update(User $user, Workspace $workspace): bool { - return $this->isOwner($user, $workspace) || $this->isAdmin($user, $workspace); + return $this->hasRole($user, $workspace, [Role::Owner, Role::Admin]); } public function delete(User $user, Workspace $workspace): bool { - return $this->isOwner($user, $workspace); + return $this->hasRole($user, $workspace, [Role::Owner]); } public function restore(User $user, Workspace $workspace): bool { - return $this->isOwner($user, $workspace); + return $this->hasRole($user, $workspace, [Role::Owner]); } public function forceDelete(User $user, Workspace $workspace): bool { - return $this->isOwner($user, $workspace); + return $this->hasRole($user, $workspace, [Role::Owner]); } public function manageTeam(User $user, Workspace $workspace): bool { - return $this->isOwner($user, $workspace) || $this->isAdmin($user, $workspace); + return $this->hasRole($user, $workspace, [Role::Owner, Role::Admin]); } public function manageAccounts(User $user, Workspace $workspace): bool { - return $this->isOwner($user, $workspace) || $this->isAdmin($user, $workspace); + return $this->hasRole($user, $workspace, [Role::Owner, Role::Admin]); } public function createPost(User $user, Workspace $workspace): bool { - return $this->isOwner($user, $workspace) || $workspace->members->contains($user); + return $this->isMember($user, $workspace); } - private function isOwner(User $user, Workspace $workspace): bool + public function manageBilling(User $user, Workspace $workspace): bool { - return $user->id === $workspace->user_id; + return $this->hasRole($user, $workspace, [Role::Owner]); } - private function isAdmin(User $user, Workspace $workspace): bool + /** + * @param Role[] $roles + */ + private function hasRole(User $user, Workspace $workspace, array $roles): bool { $member = $workspace->members()->where('user_id', $user->id)->first(); @@ -73,6 +76,11 @@ private function isAdmin(User $user, Workspace $workspace): bool return false; } - return $member->pivot->role === WorkspaceRole::Admin->value; + return in_array(Role::tryFrom($member->pivot->role), $roles); + } + + private function isMember(User $user, Workspace $workspace): bool + { + return $workspace->members()->where('user_id', $user->id)->exists(); } } diff --git a/app/Services/PostHogService.php b/app/Services/PostHogService.php index 307d155b..60d0dcc2 100644 --- a/app/Services/PostHogService.php +++ b/app/Services/PostHogService.php @@ -14,10 +14,6 @@ class PostHogService */ public function capture(string $distinctId, string $event, array $properties = []): void { - if (! config('services.posthog.api_key')) { - return; - } - $this->dispatch('capture', [ 'distinctId' => $distinctId, 'event' => $event, @@ -30,10 +26,6 @@ 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, @@ -45,10 +37,6 @@ 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, @@ -61,6 +49,10 @@ 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/app/Services/Social/BlueskyPublisher.php b/app/Services/Social/BlueskyPublisher.php index 66fb90cb..3374c994 100644 --- a/app/Services/Social/BlueskyPublisher.php +++ b/app/Services/Social/BlueskyPublisher.php @@ -94,7 +94,7 @@ public function publish(PostPlatform $postPlatform): array $data = $response->json(); // Extract post ID from URI (at://did/app.bsky.feed.post/xxx) - $uri = $data['uri']; + $uri = data_get($data, 'uri'); $postId = basename($uri); Log::info('Bluesky post created successfully', [ @@ -266,8 +266,8 @@ public function refreshToken(SocialAccount $account): void if ($response->successful()) { $data = $response->json(); $account->update([ - 'access_token' => $data['accessJwt'], - 'refresh_token' => $data['refreshJwt'], + 'access_token' => data_get($data, 'accessJwt'), + 'refresh_token' => data_get($data, 'refreshJwt'), 'token_expires_at' => now()->addHours(2), ]); @@ -295,8 +295,8 @@ public function refreshToken(SocialAccount $account): void if ($response->successful()) { $data = $response->json(); $account->update([ - 'access_token' => $data['accessJwt'], - 'refresh_token' => $data['refreshJwt'], + 'access_token' => data_get($data, 'accessJwt'), + 'refresh_token' => data_get($data, 'refreshJwt'), 'token_expires_at' => now()->addHours(2), ]); diff --git a/app/Services/Social/ConnectionVerifier.php b/app/Services/Social/ConnectionVerifier.php index 1547caec..670f625c 100644 --- a/app/Services/Social/ConnectionVerifier.php +++ b/app/Services/Social/ConnectionVerifier.php @@ -81,9 +81,9 @@ private function refreshLinkedInToken(SocialAccount $account): void $data = $response->json(); $account->update([ - 'access_token' => $data['access_token'], - 'refresh_token' => $data['refresh_token'] ?? $account->refresh_token, - 'token_expires_at' => isset($data['expires_in']) ? now()->addSeconds($data['expires_in']) : null, + 'access_token' => data_get($data, 'access_token'), + 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), + 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, ]); $account->refresh(); @@ -113,9 +113,9 @@ private function refreshXToken(SocialAccount $account): void $data = $response->json(); $account->update([ - 'access_token' => $data['access_token'], - 'refresh_token' => $data['refresh_token'] ?? $account->refresh_token, - 'token_expires_at' => now()->addSeconds($data['expires_in'] ?? 7200), + 'access_token' => data_get($data, 'access_token'), + 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), + 'token_expires_at' => now()->addSeconds(data_get($data, 'expires_in', 7200)), ]); $account->refresh(); @@ -132,8 +132,8 @@ private function refreshBlueskyToken(SocialAccount $account): void if ($response->successful()) { $data = $response->json(); $account->update([ - 'access_token' => $data['accessJwt'], - 'refresh_token' => $data['refreshJwt'], + 'access_token' => data_get($data, 'accessJwt'), + 'refresh_token' => data_get($data, 'refreshJwt'), 'token_expires_at' => now()->addHours(2), ]); @@ -156,8 +156,8 @@ private function refreshBlueskyToken(SocialAccount $account): void if ($response->successful()) { $data = $response->json(); $account->update([ - 'access_token' => $data['accessJwt'], - 'refresh_token' => $data['refreshJwt'], + 'access_token' => data_get($data, 'accessJwt'), + 'refresh_token' => data_get($data, 'refreshJwt'), 'token_expires_at' => now()->addHours(2), ]); @@ -196,8 +196,8 @@ private function refreshYouTubeToken(SocialAccount $account): void $data = $response->json(); $account->update([ - 'access_token' => $data['access_token'], - 'token_expires_at' => isset($data['expires_in']) ? now()->addSeconds($data['expires_in']) : null, + 'access_token' => data_get($data, 'access_token'), + 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, ]); $account->refresh(); @@ -224,9 +224,9 @@ private function refreshTikTokToken(SocialAccount $account): void $data = $response->json(); $account->update([ - 'access_token' => $data['access_token'], - 'refresh_token' => $data['refresh_token'] ?? $account->refresh_token, - 'token_expires_at' => isset($data['expires_in']) ? now()->addSeconds($data['expires_in']) : null, + 'access_token' => data_get($data, 'access_token'), + 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), + 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, ]); $account->refresh(); @@ -256,9 +256,9 @@ private function refreshPinterestToken(SocialAccount $account): void $data = $response->json(); $account->update([ - 'access_token' => $data['access_token'], - 'refresh_token' => $data['refresh_token'] ?? $account->refresh_token, - 'token_expires_at' => isset($data['expires_in']) ? now()->addSeconds($data['expires_in']) : null, + 'access_token' => data_get($data, 'access_token'), + 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), + 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, ]); $account->refresh(); @@ -280,8 +280,8 @@ private function refreshThreadsToken(SocialAccount $account): void $data = $response->json(); $account->update([ - 'access_token' => $data['access_token'], - 'token_expires_at' => isset($data['expires_in']) ? now()->addSeconds($data['expires_in']) : null, + 'access_token' => data_get($data, 'access_token'), + 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, ]); $account->refresh(); diff --git a/app/Services/Social/FacebookPublisher.php b/app/Services/Social/FacebookPublisher.php index efb530af..b293970d 100644 --- a/app/Services/Social/FacebookPublisher.php +++ b/app/Services/Social/FacebookPublisher.php @@ -99,7 +99,7 @@ private function publishTextPost(string $pageId, string $accessToken, string $co } $data = $response->json(); - $postId = $data['id']; + $postId = data_get($data, 'id'); return [ 'id' => $postId, @@ -126,7 +126,7 @@ private function publishSingleImagePost(string $pageId, string $accessToken, ?st } $data = $response->json(); - $postId = $data['post_id'] ?? $data['id']; + $postId = data_get($data, 'post_id', data_get($data, 'id')); return [ 'id' => $postId, @@ -192,7 +192,7 @@ private function publishMultiImagePost(string $pageId, string $accessToken, ?str } $data = $response->json(); - $postId = $data['id']; + $postId = data_get($data, 'id'); return [ 'id' => $postId, @@ -220,7 +220,7 @@ private function publishVideoPost(string $pageId, string $accessToken, ?string $ } $data = $response->json(); - $videoId = $data['id']; + $videoId = data_get($data, 'id'); return [ 'id' => $videoId, @@ -247,7 +247,7 @@ private function publishReel(string $pageId, string $accessToken, ?string $conte } $data = $response->json(); - $videoId = $data['video_id']; + $videoId = data_get($data, 'video_id'); // Upload the video file $uploadResponse = Http::post("{$this->baseUrl}/{$videoId}", [ diff --git a/app/Services/Social/LinkedInPagePublisher.php b/app/Services/Social/LinkedInPagePublisher.php index fd893eb1..a9a82ba9 100644 --- a/app/Services/Social/LinkedInPagePublisher.php +++ b/app/Services/Social/LinkedInPagePublisher.php @@ -427,7 +427,7 @@ private function waitForVideoProcessing(string $videoUrn, int $maxAttempts = 30) } $data = $response->json(); - $status = $data['status'] ?? 'UNKNOWN'; + $status = data_get($data, 'status', 'UNKNOWN'); Log::info('LinkedIn Page video processing status', ['status' => $status, 'attempt' => $i]); @@ -465,9 +465,9 @@ private function refreshToken(SocialAccount $account): void $data = $response->json(); $account->update([ - 'access_token' => $data['access_token'], - 'refresh_token' => $data['refresh_token'] ?? $account->refresh_token, - 'token_expires_at' => isset($data['expires_in']) ? now()->addSeconds($data['expires_in']) : null, + 'access_token' => data_get($data, 'access_token'), + 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), + 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, ]); // Sync tokens to LinkedIn personal if it exists diff --git a/app/Services/Social/LinkedInPublisher.php b/app/Services/Social/LinkedInPublisher.php index eaaa9973..cde8361a 100644 --- a/app/Services/Social/LinkedInPublisher.php +++ b/app/Services/Social/LinkedInPublisher.php @@ -408,7 +408,7 @@ private function waitForVideoProcessing(string $videoUrn, int $maxAttempts = 30) } $data = $response->json(); - $status = $data['status'] ?? 'UNKNOWN'; + $status = data_get($data, 'status', 'UNKNOWN'); Log::info('LinkedIn video processing status', ['status' => $status, 'attempt' => $i]); @@ -446,9 +446,9 @@ private function refreshToken(SocialAccount $account): void $data = $response->json(); $account->update([ - 'access_token' => $data['access_token'], - 'refresh_token' => $data['refresh_token'] ?? $account->refresh_token, - 'token_expires_at' => isset($data['expires_in']) ? now()->addSeconds($data['expires_in']) : null, + 'access_token' => data_get($data, 'access_token'), + 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), + 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, ]); // Sync tokens to LinkedIn Page if it exists diff --git a/app/Services/Social/MastodonPublisher.php b/app/Services/Social/MastodonPublisher.php index f6c726ea..eefce539 100644 --- a/app/Services/Social/MastodonPublisher.php +++ b/app/Services/Social/MastodonPublisher.php @@ -59,13 +59,13 @@ public function publish(PostPlatform $postPlatform): array $data = $response->json(); Log::info('Mastodon post created', [ - 'id' => $data['id'], - 'url' => $data['url'], + 'id' => data_get($data, 'id'), + 'url' => data_get($data, 'url'), ]); return [ - 'id' => $data['id'], - 'url' => $data['url'], + 'id' => data_get($data, 'id'), + 'url' => data_get($data, 'url'), ]; } @@ -100,9 +100,9 @@ private function uploadMedia(SocialAccount $account, string $instance, string $u $data = $response->json(); - Log::info('Mastodon media uploaded', ['id' => $data['id']]); + Log::info('Mastodon media uploaded', ['id' => data_get($data, 'id')]); - return $data['id']; + return data_get($data, 'id'); } catch (\Exception $e) { Log::error('Mastodon media upload error', [ 'error' => $e->getMessage(), diff --git a/app/Services/Social/PinterestPublisher.php b/app/Services/Social/PinterestPublisher.php index f15d42ca..8438075b 100644 --- a/app/Services/Social/PinterestPublisher.php +++ b/app/Services/Social/PinterestPublisher.php @@ -99,11 +99,11 @@ private function publishImagePin(PostPlatform $postPlatform): array $data = $response->json(); - Log::info('Pinterest pin created successfully', ['pin_id' => $data['id']]); + Log::info('Pinterest pin created successfully', ['pin_id' => data_get($data, 'id')]); return [ - 'id' => $data['id'], - 'url' => "https://pinterest.com/pin/{$data['id']}", + 'id' => data_get($data, 'id'), + 'url' => 'https://pinterest.com/pin/'.data_get($data, 'id'), ]; } @@ -226,11 +226,11 @@ private function publishVideoPin(PostPlatform $postPlatform): array $data = $response->json(); - Log::info('Pinterest video pin created successfully', ['pin_id' => $data['id']]); + Log::info('Pinterest video pin created successfully', ['pin_id' => data_get($data, 'id')]); return [ - 'id' => $data['id'], - 'url' => "https://pinterest.com/pin/{$data['id']}", + 'id' => data_get($data, 'id'), + 'url' => 'https://pinterest.com/pin/'.data_get($data, 'id'), ]; } @@ -292,11 +292,11 @@ private function publishCarousel(PostPlatform $postPlatform): array $data = $response->json(); - Log::info('Pinterest carousel created successfully', ['pin_id' => $data['id']]); + Log::info('Pinterest carousel created successfully', ['pin_id' => data_get($data, 'id')]); return [ - 'id' => $data['id'], - 'url' => "https://pinterest.com/pin/{$data['id']}", + 'id' => data_get($data, 'id'), + 'url' => 'https://pinterest.com/pin/'.data_get($data, 'id'), ]; } @@ -318,7 +318,7 @@ private function waitForMediaProcessing(SocialAccount $account, string $mediaId, } $data = $response->json(); - $status = $data['status'] ?? 'unknown'; + $status = data_get($data, 'status', 'unknown'); Log::info('Pinterest media processing status', [ 'media_id' => $mediaId, @@ -331,7 +331,7 @@ private function waitForMediaProcessing(SocialAccount $account, string $mediaId, } if ($status === 'failed') { - $failureCode = $data['failure_code'] ?? 'unknown'; + $failureCode = data_get($data, 'failure_code', 'unknown'); throw new \Exception("Pinterest media processing failed: {$failureCode}"); } @@ -363,9 +363,9 @@ public function refreshToken(SocialAccount $account): void $data = $response->json(); $account->update([ - 'access_token' => $data['access_token'], - 'refresh_token' => $data['refresh_token'] ?? $account->refresh_token, - 'token_expires_at' => isset($data['expires_in']) ? now()->addSeconds($data['expires_in']) : now()->addDays(30), + 'access_token' => data_get($data, 'access_token'), + 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), + 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : now()->addDays(30), ]); Log::info('Pinterest token refreshed successfully'); diff --git a/app/Services/Social/ThreadsPublisher.php b/app/Services/Social/ThreadsPublisher.php index 2111b1f7..366b80b9 100644 --- a/app/Services/Social/ThreadsPublisher.php +++ b/app/Services/Social/ThreadsPublisher.php @@ -275,7 +275,7 @@ private function waitForMediaProcessing(string $containerId, string $accessToken } $data = $statusResponse->json(); - $status = $data['status'] ?? 'UNKNOWN'; + $status = data_get($data, 'status', 'UNKNOWN'); Log::info('Threads media processing status', [ 'container_id' => $containerId, @@ -289,7 +289,7 @@ private function waitForMediaProcessing(string $containerId, string $accessToken } if ($status === 'ERROR') { - $errorMessage = $data['error_message'] ?? 'Unknown error'; + $errorMessage = data_get($data, 'error_message', 'Unknown error'); throw new \Exception('Threads media processing failed: '.$errorMessage); } @@ -316,8 +316,8 @@ private function refreshToken(SocialAccount $account): void $data = $response->json(); $account->update([ - 'access_token' => $data['access_token'], - 'token_expires_at' => isset($data['expires_in']) ? now()->addSeconds($data['expires_in']) : null, + 'access_token' => data_get($data, 'access_token'), + 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, ]); Log::info('Threads token refreshed successfully'); diff --git a/app/Services/Social/TikTokPublisher.php b/app/Services/Social/TikTokPublisher.php index f65f6eb1..2c9debf4 100644 --- a/app/Services/Social/TikTokPublisher.php +++ b/app/Services/Social/TikTokPublisher.php @@ -111,7 +111,7 @@ private function publishVideo(PostPlatform $postPlatform, $media): array Log::info('TikTok video init response', ['data' => $data]); - $publishId = $data['data']['publish_id'] ?? null; + $publishId = data_get($data, 'data')['publish_id'] ?? null; if (! $publishId) { throw new \Exception('TikTok did not return a publish_id'); @@ -172,7 +172,7 @@ private function publishPhotos(PostPlatform $postPlatform, $mediaCollection): ar Log::info('TikTok photo init response', ['data' => $data]); - $publishId = $data['data']['publish_id'] ?? null; + $publishId = data_get($data, 'data')['publish_id'] ?? null; if (! $publishId) { throw new \Exception('TikTok did not return a publish_id'); @@ -207,7 +207,7 @@ private function waitForPublishStatus(string $publishId, int $maxAttempts = 20): } $data = $response->json(); - $status = $data['data']['status'] ?? 'UNKNOWN'; + $status = data_get($data, 'data')['status'] ?? 'UNKNOWN'; Log::info('TikTok publish status', [ 'status' => $status, @@ -216,11 +216,11 @@ private function waitForPublishStatus(string $publishId, int $maxAttempts = 20): ]); if ($status === 'PUBLISH_COMPLETE') { - return $data['data'] ?? []; + return data_get($data, 'data', []); } if (in_array($status, ['FAILED', 'PUBLISH_FAILED'])) { - $errorCode = $data['data']['fail_reason'] ?? 'Unknown error'; + $errorCode = data_get($data, 'data')['fail_reason'] ?? 'Unknown error'; throw new \Exception("TikTok publish failed: {$errorCode}"); } @@ -264,9 +264,9 @@ private function refreshToken(SocialAccount $account): void $data = $response->json(); $account->update([ - 'access_token' => $data['access_token'], - 'refresh_token' => $data['refresh_token'] ?? $account->refresh_token, - 'token_expires_at' => isset($data['expires_in']) ? now()->addSeconds($data['expires_in']) : null, + 'access_token' => data_get($data, 'access_token'), + 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), + 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, ]); Log::info('TikTok token refreshed successfully'); diff --git a/app/Services/Social/XPublisher.php b/app/Services/Social/XPublisher.php index 894aeed9..7fda5841 100644 --- a/app/Services/Social/XPublisher.php +++ b/app/Services/Social/XPublisher.php @@ -60,7 +60,7 @@ public function publish(PostPlatform $postPlatform): array Log::info('X media upload response', ['response' => $uploadedMedia]); // v2 API returns data.id, v1 returns media_id - $mediaId = $uploadedMedia['data']['id'] ?? $uploadedMedia['media_id'] ?? null; + $mediaId = data_get($uploadedMedia, 'data.id', data_get($uploadedMedia, 'media_id')); if ($mediaId) { $mediaIds[] = $mediaId; } @@ -350,9 +350,9 @@ private function refreshToken(SocialAccount $account): void $data = $response->json(); $account->update([ - 'access_token' => $data['access_token'], - 'refresh_token' => $data['refresh_token'] ?? $account->refresh_token, - 'token_expires_at' => now()->addSeconds($data['expires_in'] ?? 7200), + 'access_token' => data_get($data, 'access_token'), + 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), + 'token_expires_at' => now()->addSeconds(data_get($data, 'expires_in', 7200)), ]); } diff --git a/app/Services/Social/YouTubePublisher.php b/app/Services/Social/YouTubePublisher.php index b5d45b48..6d438fbd 100644 --- a/app/Services/Social/YouTubePublisher.php +++ b/app/Services/Social/YouTubePublisher.php @@ -145,7 +145,7 @@ private function publishShort(PostPlatform $postPlatform, $media): array Log::info('YouTube upload response', ['data' => $data]); - $videoId = $data['id'] ?? null; + $videoId = data_get($data, 'id', null); if (! $videoId) { throw new \Exception('YouTube did not return a video ID'); @@ -197,9 +197,9 @@ private function refreshToken(SocialAccount $account): void $data = $response->json(); $account->update([ - 'access_token' => $data['access_token'], - 'refresh_token' => $data['refresh_token'] ?? $account->refresh_token, - 'token_expires_at' => isset($data['expires_in']) ? now()->addSeconds($data['expires_in']) : null, + 'access_token' => data_get($data, 'access_token'), + 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), + 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, ]); Log::info('YouTube token refreshed successfully'); diff --git a/app/Socialite/InstagramProvider.php b/app/Socialite/InstagramProvider.php index 2ce82ba5..bdbd9967 100644 --- a/app/Socialite/InstagramProvider.php +++ b/app/Socialite/InstagramProvider.php @@ -72,7 +72,7 @@ protected function exchangeForLongLivedToken(array $data): array RequestOptions::QUERY => [ 'grant_type' => 'ig_exchange_token', 'client_secret' => $this->clientSecret, - 'access_token' => $data['access_token'], + 'access_token' => data_get($data, 'access_token'), ], ]); diff --git a/lang/php_en.json b/lang/php_en.json new file mode 100644 index 00000000..467ed593 --- /dev/null +++ b/lang/php_en.json @@ -0,0 +1 @@ +{"auth.failed":"These credentials do not match our records.","auth.password":"The provided password is incorrect.","auth.throttle":"Too many login attempts. Please try again in :seconds seconds.","pagination.previous":"« Previous","pagination.next":"Next »","passwords.reset":"Your password has been reset.","passwords.sent":"We have emailed your password reset link.","passwords.throttled":"Please wait before retrying.","passwords.token":"This password reset token is invalid.","passwords.user":"We can't find a user with that email address.","validation.accepted":"The :attribute field must be accepted.","validation.accepted_if":"The :attribute field must be accepted when :other is :value.","validation.active_url":"The :attribute field must be a valid URL.","validation.after":"The :attribute field must be a date after :date.","validation.after_or_equal":"The :attribute field must be a date after or equal to :date.","validation.alpha":"The :attribute field must only contain letters.","validation.alpha_dash":"The :attribute field must only contain letters, numbers, dashes, and underscores.","validation.alpha_num":"The :attribute field must only contain letters and numbers.","validation.any_of":"The :attribute field is invalid.","validation.array":"The :attribute field must be an array.","validation.ascii":"The :attribute field must only contain single-byte alphanumeric characters and symbols.","validation.before":"The :attribute field must be a date before :date.","validation.before_or_equal":"The :attribute field must be a date before or equal to :date.","validation.between.array":"The :attribute field must have between :min and :max items.","validation.between.file":"The :attribute field must be between :min and :max kilobytes.","validation.between.numeric":"The :attribute field must be between :min and :max.","validation.between.string":"The :attribute field must be between :min and :max characters.","validation.boolean":"The :attribute field must be true or false.","validation.can":"The :attribute field contains an unauthorized value.","validation.confirmed":"The :attribute field confirmation does not match.","validation.contains":"The :attribute field is missing a required value.","validation.current_password":"The password is incorrect.","validation.date":"The :attribute field must be a valid date.","validation.date_equals":"The :attribute field must be a date equal to :date.","validation.date_format":"The :attribute field must match the format :format.","validation.decimal":"The :attribute field must have :decimal decimal places.","validation.declined":"The :attribute field must be declined.","validation.declined_if":"The :attribute field must be declined when :other is :value.","validation.different":"The :attribute field and :other must be different.","validation.digits":"The :attribute field must be :digits digits.","validation.digits_between":"The :attribute field must be between :min and :max digits.","validation.dimensions":"The :attribute field has invalid image dimensions.","validation.distinct":"The :attribute field has a duplicate value.","validation.doesnt_contain":"The :attribute field must not contain any of the following: :values.","validation.doesnt_end_with":"The :attribute field must not end with one of the following: :values.","validation.doesnt_start_with":"The :attribute field must not start with one of the following: :values.","validation.email":"The :attribute field must be a valid email address.","validation.encoding":"The :attribute field must be encoded in :encoding.","validation.ends_with":"The :attribute field must end with one of the following: :values.","validation.enum":"The selected :attribute is invalid.","validation.exists":"The selected :attribute is invalid.","validation.extensions":"The :attribute field must have one of the following extensions: :values.","validation.file":"The :attribute field must be a file.","validation.filled":"The :attribute field must have a value.","validation.gt.array":"The :attribute field must have more than :value items.","validation.gt.file":"The :attribute field must be greater than :value kilobytes.","validation.gt.numeric":"The :attribute field must be greater than :value.","validation.gt.string":"The :attribute field must be greater than :value characters.","validation.gte.array":"The :attribute field must have :value items or more.","validation.gte.file":"The :attribute field must be greater than or equal to :value kilobytes.","validation.gte.numeric":"The :attribute field must be greater than or equal to :value.","validation.gte.string":"The :attribute field must be greater than or equal to :value characters.","validation.hex_color":"The :attribute field must be a valid hexadecimal color.","validation.image":"The :attribute field must be an image.","validation.in":"The selected :attribute is invalid.","validation.in_array":"The :attribute field must exist in :other.","validation.in_array_keys":"The :attribute field must contain at least one of the following keys: :values.","validation.integer":"The :attribute field must be an integer.","validation.ip":"The :attribute field must be a valid IP address.","validation.ipv4":"The :attribute field must be a valid IPv4 address.","validation.ipv6":"The :attribute field must be a valid IPv6 address.","validation.json":"The :attribute field must be a valid JSON string.","validation.list":"The :attribute field must be a list.","validation.lowercase":"The :attribute field must be lowercase.","validation.lt.array":"The :attribute field must have less than :value items.","validation.lt.file":"The :attribute field must be less than :value kilobytes.","validation.lt.numeric":"The :attribute field must be less than :value.","validation.lt.string":"The :attribute field must be less than :value characters.","validation.lte.array":"The :attribute field must not have more than :value items.","validation.lte.file":"The :attribute field must be less than or equal to :value kilobytes.","validation.lte.numeric":"The :attribute field must be less than or equal to :value.","validation.lte.string":"The :attribute field must be less than or equal to :value characters.","validation.mac_address":"The :attribute field must be a valid MAC address.","validation.max.array":"The :attribute field must not have more than :max items.","validation.max.file":"The :attribute field must not be greater than :max kilobytes.","validation.max.numeric":"The :attribute field must not be greater than :max.","validation.max.string":"The :attribute field must not be greater than :max characters.","validation.max_digits":"The :attribute field must not have more than :max digits.","validation.mimes":"The :attribute field must be a file of type: :values.","validation.mimetypes":"The :attribute field must be a file of type: :values.","validation.min.array":"The :attribute field must have at least :min items.","validation.min.file":"The :attribute field must be at least :min kilobytes.","validation.min.numeric":"The :attribute field must be at least :min.","validation.min.string":"The :attribute field must be at least :min characters.","validation.min_digits":"The :attribute field must have at least :min digits.","validation.missing":"The :attribute field must be missing.","validation.missing_if":"The :attribute field must be missing when :other is :value.","validation.missing_unless":"The :attribute field must be missing unless :other is :value.","validation.missing_with":"The :attribute field must be missing when :values is present.","validation.missing_with_all":"The :attribute field must be missing when :values are present.","validation.multiple_of":"The :attribute field must be a multiple of :value.","validation.not_in":"The selected :attribute is invalid.","validation.not_regex":"The :attribute field format is invalid.","validation.numeric":"The :attribute field must be a number.","validation.password.letters":"The :attribute field must contain at least one letter.","validation.password.mixed":"The :attribute field must contain at least one uppercase and one lowercase letter.","validation.password.numbers":"The :attribute field must contain at least one number.","validation.password.symbols":"The :attribute field must contain at least one symbol.","validation.password.uncompromised":"The given :attribute has appeared in a data leak. Please choose a different :attribute.","validation.present":"The :attribute field must be present.","validation.present_if":"The :attribute field must be present when :other is :value.","validation.present_unless":"The :attribute field must be present unless :other is :value.","validation.present_with":"The :attribute field must be present when :values is present.","validation.present_with_all":"The :attribute field must be present when :values are present.","validation.prohibited":"The :attribute field is prohibited.","validation.prohibited_if":"The :attribute field is prohibited when :other is :value.","validation.prohibited_if_accepted":"The :attribute field is prohibited when :other is accepted.","validation.prohibited_if_declined":"The :attribute field is prohibited when :other is declined.","validation.prohibited_unless":"The :attribute field is prohibited unless :other is in :values.","validation.prohibits":"The :attribute field prohibits :other from being present.","validation.regex":"The :attribute field format is invalid.","validation.required":"The :attribute field is required.","validation.required_array_keys":"The :attribute field must contain entries for: :values.","validation.required_if":"The :attribute field is required when :other is :value.","validation.required_if_accepted":"The :attribute field is required when :other is accepted.","validation.required_if_declined":"The :attribute field is required when :other is declined.","validation.required_unless":"The :attribute field is required unless :other is in :values.","validation.required_with":"The :attribute field is required when :values is present.","validation.required_with_all":"The :attribute field is required when :values are present.","validation.required_without":"The :attribute field is required when :values is not present.","validation.required_without_all":"The :attribute field is required when none of :values are present.","validation.same":"The :attribute field must match :other.","validation.size.array":"The :attribute field must contain :size items.","validation.size.file":"The :attribute field must be :size kilobytes.","validation.size.numeric":"The :attribute field must be :size.","validation.size.string":"The :attribute field must be :size characters.","validation.starts_with":"The :attribute field must start with one of the following: :values.","validation.string":"The :attribute field must be a string.","validation.timezone":"The :attribute field must be a valid timezone.","validation.unique":"The :attribute has already been taken.","validation.uploaded":"The :attribute failed to upload.","validation.uppercase":"The :attribute field must be uppercase.","validation.url":"The :attribute field must be a valid URL.","validation.ulid":"The :attribute field must be a valid ULID.","validation.uuid":"The :attribute field must be a valid UUID.","validation.custom.attribute-name.rule-name":"custom-message","accounts.title":"Connections","accounts.page_title":"Connected Accounts","accounts.description":"Connect your social networks to schedule and publish posts","accounts.not_connected":"Not connected","accounts.connect":"Connect","accounts.connection_lost":"Connection lost","accounts.reconnect_account":"Reconnect account","accounts.view_profile":"View profile","accounts.disconnect":"Disconnect","accounts.disconnect_modal.title":"Disconnect Account","accounts.disconnect_modal.description":"Are you sure you want to disconnect this account? You can reconnect it at any time.","accounts.disconnect_modal.confirm":"Disconnect","accounts.disconnect_modal.cancel":"Cancel","accounts.bluesky.title":"Connect Bluesky","accounts.bluesky.description":"Enter your credentials to connect","accounts.bluesky.email":"Email","accounts.bluesky.email_placeholder":"yourhandle.bsky.social","accounts.bluesky.app_password":"App Password","accounts.bluesky.app_password_placeholder":"xxxx-xxxx-xxxx-xxxx","accounts.bluesky.app_password_hint":"Use an App Password for security. Create one at bsky.app/settings.","accounts.bluesky.submit":"Connect Bluesky","accounts.bluesky.submitting":"Connecting...","accounts.mastodon.title":"Connect Mastodon","accounts.mastodon.description":"Enter your Mastodon instance","accounts.mastodon.instance_url":"Instance URL","accounts.mastodon.instance_placeholder":"https://mastodon.social","accounts.mastodon.instance_hint":"Enter your Mastodon instance URL (e.g., mastodon.social, techhub.social)","accounts.mastodon.submit":"Continue with Mastodon","accounts.mastodon.submitting":"Connecting...","accounts.facebook.title":"Select Facebook Page","accounts.facebook.description":"Choose which page you want to connect","accounts.facebook.no_pages":"No pages found","accounts.facebook.no_pages_description":"You are not an admin of any Facebook page.","accounts.facebook.page_label":"Facebook Page","accounts.linkedin.title":"Select LinkedIn Page","accounts.linkedin.description":"Choose which page you want to connect","accounts.linkedin.no_pages":"No pages found","accounts.linkedin.no_pages_description":"You are not an administrator of any LinkedIn page.","accounts.linkedin.page_label":"LinkedIn Page","accounts.flash.disconnected":"Account disconnected successfully!","accounts.flash.connected":"Account connected successfully!","accounts.flash.session_expired":"Session expired. Please try again.","accounts.flash.workspace_not_found":"Workspace not found.","accounts.flash.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.","auth.flash.welcome":"Welcome to TryPost!","auth.flash.welcome_trial":"Welcome to TryPost! Your trial has started.","auth.legal":"By continuing, you agree to our Terms of Service and Privacy Policy.","auth.slides.calendar.title":"Visual Calendar","auth.slides.calendar.description":"Plan and schedule your content with an intuitive drag-and-drop calendar across all your social accounts.","auth.slides.scheduling.title":"Smart Scheduling","auth.slides.scheduling.description":"Schedule posts across LinkedIn, X, Instagram, TikTok, YouTube, and more — all from one place.","auth.slides.media.title":"Rich Media","auth.slides.media.description":"Publish images, carousels, stories, and reels. Each platform gets the right format automatically.","auth.slides.video.title":"Video Publishing","auth.slides.video.description":"Upload videos once and publish to TikTok, YouTube Shorts, Instagram Reels, and Facebook Reels.","auth.slides.team.title":"Team Workspaces","auth.slides.team.description":"Invite your team, assign roles, and manage multiple brands from separate workspaces.","auth.slides.hashtags.title":"Hashtag Groups","auth.slides.hashtags.description":"Save hashtag collections and add them to posts with one click. Organize with labels and filters.","auth.or_continue_with":"Or continue with","auth.google_login":"Log in with Google","auth.google_signup":"Sign up with Google","auth.signup_success.page_title":"Welcome","auth.signup_success.title":"Setting up your account","auth.signup_success.description":"This usually takes just a few seconds...","auth.login.title":"Log in to your account","auth.login.description":"Enter your email and password below to log in","auth.login.page_title":"Log in","auth.login.email":"Email address","auth.login.password":"Password","auth.login.forgot_password":"Forgot password?","auth.login.remember_me":"Remember me","auth.login.submit":"Log in","auth.login.no_account":"Don't have an account?","auth.login.sign_up":"Sign up","auth.register.title":"Create an account","auth.register.description":"Enter your details below to create your account","auth.register.page_title":"Register","auth.register.name":"Name","auth.register.name_placeholder":"Full name","auth.register.email":"Email address","auth.register.password":"Password","auth.register.show_password":"Show password","auth.register.hide_password":"Hide password","auth.register.submit":"Create account","auth.register.has_account":"Already have an account?","auth.register.log_in":"Log in","auth.forgot_password.title":"Forgot password","auth.forgot_password.description":"Enter your email to receive a password reset link","auth.forgot_password.page_title":"Forgot password","auth.forgot_password.email":"Email address","auth.forgot_password.submit":"Email password reset link","auth.forgot_password.return_to":"Or, return to","auth.forgot_password.log_in":"log in","auth.reset_password.title":"Reset password","auth.reset_password.description":"Please enter your new password below","auth.reset_password.page_title":"Reset password","auth.reset_password.email":"Email","auth.reset_password.password":"Password","auth.reset_password.confirm_password":"Confirm Password","auth.reset_password.confirm_placeholder":"Confirm password","auth.reset_password.submit":"Reset password","auth.verify_email.title":"Verify email","auth.verify_email.description":"Please verify your email address by clicking on the link we just emailed to you.","auth.verify_email.page_title":"Email verification","auth.verify_email.link_sent":"A new verification link has been sent to the email address you provided during registration.","auth.verify_email.resend":"Resend verification email","auth.verify_email.log_out":"Log out","auth.accept_invite.page_title":"Accept Invite","auth.accept_invite.title":"You've been invited!","auth.accept_invite.description":"You've been invited to join the :workspace workspace.","auth.accept_invite.workspace":"Workspace","auth.accept_invite.your_role":"Your role","auth.accept_invite.email":"Email","auth.accept_invite.accept":"Accept Invite","auth.accept_invite.decline":"Decline Invite","auth.accept_invite.login_prompt":"Log in or create an account to accept this invite.","auth.accept_invite.log_in":"Log in","auth.accept_invite.create_account":"Create Account","billing.title":"Subscription","billing.description":"Manage your subscription and payment method","billing.subscribe.page_title":"Start your free trial","billing.subscribe.title":"Start your free trial","billing.subscribe.description":":days days free. No credit card required to start.","billing.subscribe.start_trial":"Start :days-day free trial","billing.subscribe.cancel_anytime":"Cancel anytime. No questions asked.","billing.subscribe.switch_workspace":"Switch workspace","billing.subscribe.features.calendar":"Visual drag-and-drop calendar","billing.subscribe.features.scheduling":"Unlimited post scheduling","billing.subscribe.features.media":"Images, carousels & stories","billing.subscribe.features.video":"Video publishing across platforms","billing.subscribe.features.team":"Team collaboration & workspaces","billing.subscribe.features.hashtags":"Hashtag groups & labels","billing.trial.title":"Trial period active","billing.trial.description":"Your trial ends on :date. After that, your subscription will be charged automatically.","billing.subscription.title":"Your Subscription","billing.subscription.workspaces":"Workspaces","billing.subscription.quantity":"Subscription quantity","billing.subscription.expires":"Expires :date","billing.subscription.canceled_on":"Your subscription will be canceled on :date","billing.subscription.manage":"Manage on Stripe","billing.invoices.title":"Invoices","billing.invoices.description":"Payment history","billing.invoices.empty":"No invoices found","billing.invoices.paid":"Paid","billing.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","billing.status.active":"Active","billing.status.canceled":"Canceled","billing.status.incomplete":"Incomplete","billing.status.incomplete_expired":"Expired","billing.status.past_due":"Past due","billing.status.trialing":"Trial","billing.status.unpaid":"Unpaid","calendar.title":"Calendar","calendar.today":"Today","calendar.day":"Day","calendar.week":"Week","calendar.month":"Month","calendar.new_post":"New Post","calendar.no_content":"No content","calendar.more":"+:count more","common.confirm_modal.cannot_be_undone":"This cannot be undone.","common.confirm_modal.type":"Type","common.confirm_modal.to_confirm":"to confirm.","common.confirm_modal.copy_to_clipboard":"Copy to clipboard","common.photo_upload.upload":"Upload","common.photo_upload.uploading":"Uploading...","common.photo_upload.remove":"Remove photo","common.photo_upload.hint":"Recommended: square image, max 2 MB.","common.timezone.select":"Select timezone","common.timezone.search":"Search timezone...","common.timezone.empty":"No timezone found","common.date_picker.select":"Select date","common.cancel":"Cancel","hashtags.title":"Hashtags","hashtags.description":"Create hashtag groups to quickly add to your posts","hashtags.search":"Search hashtags...","hashtags.new_group":"New Group","hashtags.no_groups_yet":"No hashtag groups yet","hashtags.no_groups_description":"Create hashtag groups to quickly add popular hashtags to your posts","hashtags.create_first_group":"Create your first group","hashtags.hashtags_count":":count hashtags","hashtags.create.title":"Create Hashtag Group","hashtags.create.description":"Give your group a name and add hashtags separated by spaces or commas","hashtags.create.name":"Group Name","hashtags.create.name_placeholder":"e.g. Marketing, Travel, Food","hashtags.create.hashtags":"Hashtags","hashtags.create.hashtags_placeholder":"#marketing #socialmedia #business #growth","hashtags.create.hashtags_hint":"Enter hashtags separated by spaces or commas. Include the # symbol.","hashtags.create.submit":"Create Group","hashtags.create.submitting":"Creating...","hashtags.edit.title":"Edit Hashtag Group","hashtags.edit.description":"Update the name and hashtags for this group","hashtags.edit.name":"Group Name","hashtags.edit.name_placeholder":"e.g. Marketing, Travel, Food","hashtags.edit.hashtags":"Hashtags","hashtags.edit.hashtags_placeholder":"#marketing #socialmedia #business #growth","hashtags.edit.hashtags_hint":"Enter hashtags separated by spaces or commas. Include the # symbol.","hashtags.edit.submit":"Save Changes","hashtags.edit.submitting":"Saving...","hashtags.delete.title":"Delete Hashtag Group","hashtags.delete.description":"Are you sure you want to delete this hashtag group? This action cannot be undone.","hashtags.delete.confirm":"Delete","hashtags.delete.cancel":"Cancel","hashtags.flash.created":"Hashtag group created successfully!","hashtags.flash.updated":"Hashtag group updated successfully!","hashtags.flash.deleted":"Hashtag group deleted successfully!","labels.title":"Labels","labels.description":"Create labels to organize and categorize your posts","labels.search":"Search labels...","labels.new_label":"New Label","labels.no_labels_yet":"No labels yet","labels.create_first_label":"Create your first label","labels.create.title":"Create Label","labels.create.description":"Give your label a name and pick a color","labels.create.name":"Name","labels.create.name_placeholder":"Enter label name...","labels.create.color":"Color","labels.create.submit":"Create Label","labels.create.submitting":"Creating...","labels.edit.title":"Edit Label","labels.edit.description":"Update the name and color for this label","labels.edit.name":"Name","labels.edit.name_placeholder":"Enter label name...","labels.edit.color":"Color","labels.edit.submit":"Save Changes","labels.edit.submitting":"Saving...","labels.delete.title":"Delete Label","labels.delete.description":"Are you sure you want to delete this label? This action cannot be undone.","labels.delete.confirm":"Delete","labels.delete.cancel":"Cancel","labels.flash.created":"Label created successfully!","labels.flash.updated":"Label updated successfully!","labels.flash.deleted":"Label deleted successfully!","mail.workspace_connections_disconnected.subject":"{1} :count account needs to be reconnected in :workspace|[2,*] :count accounts need to be reconnected in :workspace","mail.workspace_connections_disconnected.title":"Accounts Need Reconnection","mail.workspace_connections_disconnected.intro":"The following social accounts in your :workspace workspace have been disconnected and need to be reconnected:","mail.workspace_connections_disconnected.reasons_title":"This may have happened because:","mail.workspace_connections_disconnected.reason_expired":"Access tokens expired","mail.workspace_connections_disconnected.reason_revoked":"You revoked access to TryPost on the platform","mail.workspace_connections_disconnected.reason_changed":"The platform changed their authentication requirements","mail.workspace_connections_disconnected.reconnect_cta":"Please reconnect these accounts to continue scheduling and publishing posts.","mail.workspace_connections_disconnected.button":"Reconnect Accounts","onboarding.role.page_title":"Welcome - Tell us about yourself","onboarding.role.title":"Tell us about yourself","onboarding.role.description":"Help us personalize your experience","onboarding.role.submit":"Continue","onboarding.connect.page_title":"Connect your accounts","onboarding.connect.title":"Connect your accounts","onboarding.connect.description":"Connect at least one social network to get started","onboarding.connect.submit":"Continue","onboarding.connect.error":"Something went wrong. Please try again.","onboarding.connect.go_back":"Go Back","posts.title":"Posts","posts.search":"Search posts...","posts.all_posts":"All Posts","posts.new_post":"New Post","posts.no_posts":"No posts found","posts.start_creating":"Start by creating your first post.","posts.manage_posts":"Manage all your posts","posts.delete_confirm":"Are you sure you want to delete this post?","posts.by":"by","posts.actions.view":"View post","posts.actions.delete":"Delete post","posts.form.post_type":"Post Type","posts.form.board":"Board","posts.form.select_board":"Select a board","posts.form.search_board":"Search board...","posts.form.no_board_found":"No board found","posts.form.media":"Media","posts.form.min":"Min","posts.form.uploading":"Uploading...","posts.form.drop_to_upload":"Drop to upload","posts.form.drag_and_drop":"Drag & drop or click to upload","posts.form.photos_and_videos":"Photos and videos","posts.form.photos_only":"Photos only","posts.form.videos_only":"Videos only","posts.form.drag_to_reorder":"Drag to reorder","posts.form.caption":"Caption","posts.form.write_caption":"Write your caption...","posts.status.pending":"Pending","posts.status.draft":"Draft","posts.status.scheduled":"Scheduled","posts.status.publishing":"Publishing","posts.status.published":"Published","posts.status.partially_published":"Partially Published","posts.status.failed":"Failed","posts.descriptions.draft":"Posts waiting to be scheduled","posts.descriptions.scheduled":"Posts scheduled for publishing","posts.descriptions.published":"Posts already published","posts.edit.title":"Edit Post","posts.edit.view_title":"View Post","posts.edit.manage_platforms":"Manage platforms","posts.edit.sync":"Sync","posts.edit.labels":"Labels","posts.edit.hashtags":"Hashtags","posts.edit.schedule":"Schedule","posts.edit.publish":"Publish","posts.edit.delete":"Delete","posts.edit.settings":"Settings","posts.edit.schedule_for":"Schedule for","posts.edit.saving":"Saving...","posts.edit.saved":"Saved","posts.edit.scheduled_at":"Scheduled:","posts.edit.published_at":"Published:","posts.edit.media":"Media","posts.edit.caption":"Caption","posts.edit.no_caption":"No caption","posts.edit.no_content":"No content","posts.edit.empty_state.title":"No platforms selected","posts.edit.empty_state.description":"Select at least one platform to create your post","posts.edit.delete_modal.title":"Delete Post","posts.edit.delete_modal.description":"Are you sure you want to delete this post? This action cannot be undone.","posts.edit.delete_modal.action":"Delete","posts.edit.delete_modal.cancel":"Cancel","posts.edit.sync_enable.title":"Enable sync?","posts.edit.sync_enable.description":"All platforms will share the same content. Any custom edits made to individual platforms will be replaced with the current content.","posts.edit.sync_enable.cancel":"Cancel","posts.edit.sync_enable.action":"Enable sync","posts.edit.sync_disable.title":"Disable sync?","posts.edit.sync_disable.description":"Each platform will keep its current content, but future edits will only apply to the platform you're editing.","posts.edit.sync_disable.customize_note":"You'll be able to customize the content for each platform individually.","posts.edit.sync_disable.cancel":"Cancel","posts.edit.sync_disable.action":"Disable sync","posts.edit.platforms_dialog.title":"Select Platforms","posts.edit.platforms_dialog.description":"Choose which platforms to publish this post to.","posts.edit.hashtags_modal.search":"Search hashtags...","posts.edit.hashtags_modal.no_results":"No hashtags found.","posts.edit.validation.select_board":"Select a board","posts.edit.validation.images_not_supported":"Images not supported","posts.edit.validation.videos_not_supported":"Videos not supported","posts.edit.validation.max_images":"Max :count images","posts.edit.validation.requires_media":"Requires media","posts.edit.validation.exceeded":":count exceeded","posts.edit.validation.does_not_support_images":":platform does not support images","posts.edit.validation.supports_up_to_images":":platform supports up to :count images","posts.edit.validation.does_not_support_videos":":platform does not support videos","posts.content_types.instagram_feed.label":"Feed Post","posts.content_types.instagram_feed.description":"Appears in your feed and profile","posts.content_types.instagram_reel.label":"Reel","posts.content_types.instagram_reel.description":"Short video up to 90 seconds","posts.content_types.instagram_story.label":"Story","posts.content_types.instagram_story.description":"Disappears after 24 hours","posts.content_types.linkedin_post.label":"Post","posts.content_types.linkedin_post.description":"Standard post with text and media","posts.content_types.linkedin_carousel.label":"Carousel","posts.content_types.linkedin_carousel.description":"Swipeable images","posts.content_types.linkedin_page_post.label":"Post","posts.content_types.linkedin_page_post.description":"Standard post with text and media","posts.content_types.linkedin_page_carousel.label":"Carousel","posts.content_types.linkedin_page_carousel.description":"Swipeable images","posts.content_types.facebook_post.label":"Post","posts.content_types.facebook_post.description":"Standard post on your page","posts.content_types.facebook_reel.label":"Reel","posts.content_types.facebook_reel.description":"Short video up to 90 seconds","posts.content_types.facebook_story.label":"Story","posts.content_types.facebook_story.description":"Disappears after 24 hours","posts.content_types.tiktok_video.label":"Video","posts.content_types.tiktok_video.description":"Short-form video content","posts.content_types.youtube_short.label":"Short","posts.content_types.youtube_short.description":"Vertical video up to 60 seconds","posts.content_types.x_post.label":"Post","posts.content_types.x_post.description":"Tweet with text and media","posts.content_types.threads_post.label":"Post","posts.content_types.threads_post.description":"Text post with optional media","posts.content_types.pinterest_pin.label":"Pin","posts.content_types.pinterest_pin.description":"Image pin with link","posts.content_types.pinterest_video_pin.label":"Video Pin","posts.content_types.pinterest_video_pin.description":"Video content","posts.content_types.pinterest_carousel.label":"Carousel","posts.content_types.pinterest_carousel.description":"2-5 images","posts.content_types.bluesky_post.label":"Post","posts.content_types.bluesky_post.description":"Text post with optional images","posts.content_types.mastodon_post.label":"Post","posts.content_types.mastodon_post.description":"Text post with optional media","posts.platforms.linkedin":"LinkedIn","posts.platforms.linkedin-page":"LinkedIn Page","posts.platforms.x":"X","posts.platforms.tiktok":"TikTok","posts.platforms.youtube":"YouTube Shorts","posts.platforms.facebook":"Facebook Page","posts.platforms.instagram":"Instagram","posts.platforms.threads":"Threads","posts.platforms.pinterest":"Pinterest","posts.platforms.bluesky":"Bluesky","posts.platforms.mastodon":"Mastodon","posts.flash.scheduled":"Post scheduled successfully!","posts.flash.publishing":"Post is being published!","posts.flash.deleted":"Post deleted successfully!","posts.flash.cannot_edit_published":"Published posts cannot be edited.","posts.flash.connect_first":"Connect at least one social network before creating a post.","posts.errors.account_disconnected":"Social account is disconnected","posts.errors.account_inactive":"Social account is deactivated","settings.title":"Settings","settings.description":"Manage your profile and account settings","settings.nav.profile":"Profile","settings.nav.password":"Password","settings.nav.workspace":"Workspace","settings.nav.members":"Members","settings.nav.notifications":"Notifications","settings.nav.billing":"Billing","settings.notifications.title":"Notification preferences","settings.notifications.heading":"Email notifications","settings.notifications.description":"Choose which email notifications you want to receive","settings.notifications.post_published":"Post published","settings.notifications.post_published_description":"Receive an email when your post is published successfully","settings.notifications.post_failed":"Post failed","settings.notifications.post_failed_description":"Receive an email when your post fails to publish","settings.notifications.account_disconnected":"Account disconnected","settings.notifications.account_disconnected_description":"Receive an email when a social account is disconnected","settings.notifications.save":"Save preferences","settings.profile.title":"Profile settings","settings.profile.photo_heading":"Profile photo","settings.profile.photo_description":"Upload a profile photo","settings.profile.heading":"Profile information","settings.profile.description":"Update your name and email address","settings.profile.avatar":"Avatar","settings.profile.name":"Name","settings.profile.name_placeholder":"Full name","settings.profile.email":"Email address","settings.profile.email_placeholder":"Email address","settings.profile.email_unverified":"Your email address is unverified.","settings.profile.resend_verification":"Click here to resend the verification email.","settings.profile.verification_sent":"A new verification link has been sent to your email address.","settings.profile.save":"Save","settings.password.title":"Password settings","settings.password.heading":"Update password","settings.password.description":"Ensure your account is using a long, random password to stay secure","settings.password.current_password":"Current password","settings.password.current_password_placeholder":"Current password","settings.password.new_password":"New password","settings.password.new_password_placeholder":"New password","settings.password.confirm_password":"Confirm password","settings.password.confirm_password_placeholder":"Confirm password","settings.password.save":"Save password","settings.delete_account.heading":"Delete account","settings.delete_account.description":"Delete your account and all of its resources","settings.delete_account.warning":"Warning","settings.delete_account.warning_message":"Please proceed with caution, this cannot be undone.","settings.delete_account.button":"Delete account","settings.delete_account.modal_title":"Are you sure you want to delete your account?","settings.delete_account.modal_description":"Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.","settings.delete_account.password":"Password","settings.delete_account.password_placeholder":"Password","settings.delete_account.cancel":"Cancel","settings.delete_account.confirm":"Delete account","settings.workspace.title":"Workspace settings","settings.workspace.logo_heading":"Workspace logo","settings.workspace.logo_description":"Upload a logo for your workspace","settings.workspace.heading":"Workspace name","settings.workspace.description":"Update your workspace name and timezone","settings.workspace.members_heading":"Members","settings.workspace.members_description":"Manage workspace members and invitations","settings.workspace.name":"Name","settings.workspace.name_placeholder":"My Workspace","settings.workspace.timezone":"Timezone","settings.workspace.save":"Save","settings.members.title":"Members","settings.members.heading":"Team members","settings.members.description":"Manage members and invites for this workspace","settings.members.cancel":"Cancel","settings.members.remove":"Remove","settings.members.make_admin":"Make admin","settings.members.make_member":"Make member","settings.members.invite.title":"Invite Member","settings.members.invite.description":"Send an email invite to add collaborators","settings.members.invite.email":"Email","settings.members.invite.email_placeholder":"collaborator@email.com","settings.members.invite.role":"Role","settings.members.invite.role_placeholder":"Select a role","settings.members.invite.submit":"Send Invite","settings.members.pending.title":"Pending Invites","settings.members.pending.description":"Invites awaiting acceptance","settings.members.pending.empty":"No pending invites","settings.members.list.title":"Members","settings.members.list.description":"People with access to this workspace","settings.members.list.empty":"No members besides the owner","settings.members.remove_modal.title":"Remove member","settings.members.remove_modal.description":"Are you sure you want to remove this member from the workspace? They will lose access to all workspace resources.","settings.members.remove_modal.action":"Remove member","settings.members.cancel_invite_modal.title":"Cancel invitation","settings.members.cancel_invite_modal.description":"Are you sure you want to cancel this invitation?","settings.members.cancel_invite_modal.action":"Cancel invitation","settings.members.roles.owner":"Owner","settings.members.roles.admin":"Admin","settings.members.roles.member":"Member","settings.members.flash.invite_sent":"Invite sent successfully!","settings.members.flash.invite_deleted":"Invite deleted.","settings.members.flash.member_removed":"Member removed successfully.","settings.members.flash.role_updated":"Member role updated.","settings.members.flash.wrong_email":"This invite is for a different email address.","settings.members.flash.already_member":"You are already a member of this workspace.","settings.members.flash.invite_accepted":"Welcome! You are now a member of the workspace.","settings.members.flash.invite_declined":"Invite declined.","settings.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.account":"Account","sidebar.log_out":"Log out","sidebar.workspace":"Workspace: :name","sidebar.workspace_select":"Workspace: Select","sidebar.theme":"Theme: :name","sidebar.theme_light":"Light","sidebar.theme_dark":"Dark","sidebar.theme_system":"System","sidebar.language":"Language: :name","sidebar.language_select":"Language: Select","sidebar.groups.posts":"Posts","sidebar.groups.configuration":"Configurations","sidebar.groups.support":"Support","sidebar.posts.calendar":"Calendar","sidebar.posts.all":"All","sidebar.posts.scheduled":"Scheduled","sidebar.posts.posted":"Posted","sidebar.posts.drafts":"Drafts","sidebar.config.connections":"Connections","sidebar.config.hashtags":"Hashtags","sidebar.config.labels":"Labels","sidebar.config.settings":"Settings","sidebar.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","workspaces.title":"Workspaces","workspaces.select_title":"Your workspaces","workspaces.select_description":"Select a workspace to continue","workspaces.current":"Current","workspaces.connections":":count connections","workspaces.posts":":count posts","workspaces.create.title":"Create a workspace","workspaces.create.description":"Give your workspace a name to get started","workspaces.create.name":"Workspace name","workspaces.create.name_placeholder":"My Workspace","workspaces.create.submit":"Create workspace"} \ No newline at end of file diff --git a/lang/php_es.json b/lang/php_es.json new file mode 100644 index 00000000..38ddfcee --- /dev/null +++ b/lang/php_es.json @@ -0,0 +1 @@ +{"accounts.title":"Conexiones","accounts.page_title":"Cuentas conectadas","accounts.description":"Conecta tus redes sociales para programar y publicar posts","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.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.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.","auth.failed":"Estas credenciales no coinciden con nuestros registros.","auth.password":"La contraseña proporcionada es incorrecta.","auth.throttle":"Demasiados intentos de inicio de sesión. Inténtalo de nuevo en :seconds segundos.","auth.flash.welcome":"¡Bienvenido a TryPost!","auth.flash.welcome_trial":"¡Bienvenido a TryPost! Tu prueba ha comenzado.","auth.legal":"Al continuar, aceptas nuestros Términos de Servicio y Política de Privacidad.","auth.slides.calendar.title":"Calendario Visual","auth.slides.calendar.description":"Planifica y programa tu contenido con un calendario intuitivo de arrastrar y soltar en todas tus cuentas sociales.","auth.slides.scheduling.title":"Programación Inteligente","auth.slides.scheduling.description":"Programa posts en LinkedIn, X, Instagram, TikTok, YouTube y más — todo desde un solo lugar.","auth.slides.media.title":"Contenido Multimedia","auth.slides.media.description":"Publica imágenes, carruseles, historias y reels. Cada plataforma recibe el formato correcto automáticamente.","auth.slides.video.title":"Publicación de Video","auth.slides.video.description":"Sube videos una vez y publícalos en TikTok, YouTube Shorts, Instagram Reels y Facebook Reels.","auth.slides.team.title":"Workspaces en Equipo","auth.slides.team.description":"Invita a tu equipo, asigna roles y gestiona múltiples marcas en workspaces separados.","auth.slides.hashtags.title":"Grupos de Hashtags","auth.slides.hashtags.description":"Guarda colecciones de hashtags y agrégalos a tus posts con un clic. Organiza con etiquetas y filtros.","auth.or_continue_with":"O continuar con","auth.google_login":"Iniciar sesión con Google","auth.google_signup":"Registrarse con Google","auth.signup_success.page_title":"Bienvenido","auth.signup_success.title":"Configurando tu cuenta","auth.signup_success.description":"Esto suele tardar solo unos segundos...","auth.login.title":"Inicia sesión en tu cuenta","auth.login.description":"Introduce tu correo y contraseña para iniciar sesión","auth.login.page_title":"Iniciar sesión","auth.login.email":"Correo electrónico","auth.login.password":"Contraseña","auth.login.forgot_password":"¿Olvidaste tu contraseña?","auth.login.remember_me":"Recuérdame","auth.login.submit":"Iniciar sesión","auth.login.no_account":"¿No tienes una cuenta?","auth.login.sign_up":"Regístrate","auth.register.title":"Crear una cuenta","auth.register.description":"Introduce tus datos para crear tu cuenta","auth.register.page_title":"Registro","auth.register.name":"Nombre","auth.register.name_placeholder":"Nombre completo","auth.register.email":"Correo electrónico","auth.register.password":"Contraseña","auth.register.show_password":"Mostrar contraseña","auth.register.hide_password":"Ocultar contraseña","auth.register.submit":"Crear cuenta","auth.register.has_account":"¿Ya tienes una cuenta?","auth.register.log_in":"Iniciar sesión","auth.forgot_password.title":"Olvidé mi contraseña","auth.forgot_password.description":"Introduce tu correo para recibir un enlace de restablecimiento","auth.forgot_password.page_title":"Olvidé mi contraseña","auth.forgot_password.email":"Correo electrónico","auth.forgot_password.submit":"Enviar enlace de restablecimiento","auth.forgot_password.return_to":"O vuelve a","auth.forgot_password.log_in":"iniciar sesión","auth.reset_password.title":"Restablecer contraseña","auth.reset_password.description":"Introduce tu nueva contraseña","auth.reset_password.page_title":"Restablecer contraseña","auth.reset_password.email":"Correo electrónico","auth.reset_password.password":"Contraseña","auth.reset_password.confirm_password":"Confirmar contraseña","auth.reset_password.confirm_placeholder":"Confirmar contraseña","auth.reset_password.submit":"Restablecer contraseña","auth.verify_email.title":"Verificar correo","auth.verify_email.description":"Verifica tu correo electrónico haciendo clic en el enlace que acabamos de enviarte.","auth.verify_email.page_title":"Verificación de correo","auth.verify_email.link_sent":"Se ha enviado un nuevo enlace de verificación al correo electrónico proporcionado durante el registro.","auth.verify_email.resend":"Reenviar correo de verificación","auth.verify_email.log_out":"Cerrar sesión","auth.accept_invite.page_title":"Aceptar invitación","auth.accept_invite.title":"¡Has sido invitado!","auth.accept_invite.description":"Has sido invitado a unirte al workspace :workspace.","auth.accept_invite.workspace":"Workspace","auth.accept_invite.your_role":"Tu rol","auth.accept_invite.email":"Correo electrónico","auth.accept_invite.accept":"Aceptar invitación","auth.accept_invite.decline":"Rechazar invitación","auth.accept_invite.login_prompt":"Inicia sesión o crea una cuenta para aceptar esta invitación.","auth.accept_invite.log_in":"Iniciar sesión","auth.accept_invite.create_account":"Crear cuenta","billing.title":"Suscripción","billing.description":"Administra tu suscripción y método de pago","billing.subscribe.page_title":"Comienza tu prueba gratuita","billing.subscribe.title":"Comienza tu prueba gratuita","billing.subscribe.description":":days días gratis. Sin tarjeta de crédito para empezar.","billing.subscribe.start_trial":"Comenzar prueba de :days días","billing.subscribe.cancel_anytime":"Cancela en cualquier momento. Sin preguntas.","billing.subscribe.switch_workspace":"Cambiar workspace","billing.subscribe.features.calendar":"Calendario visual con arrastrar y soltar","billing.subscribe.features.scheduling":"Programación ilimitada de posts","billing.subscribe.features.media":"Imágenes, carruseles e historias","billing.subscribe.features.video":"Publicación de videos en todas las plataformas","billing.subscribe.features.team":"Colaboración en equipo y workspaces","billing.subscribe.features.hashtags":"Grupos de hashtags y etiquetas","billing.trial.title":"Periodo de prueba activo","billing.trial.description":"Tu prueba termina el :date. Después, tu suscripción se cobrará automáticamente.","billing.subscription.title":"Tu suscripción","billing.subscription.workspaces":"Workspaces","billing.subscription.quantity":"Cantidad de suscripción","billing.subscription.expires":"Expira :date","billing.subscription.canceled_on":"Tu suscripción se cancelará el :date","billing.subscription.manage":"Administrar en Stripe","billing.invoices.title":"Facturas","billing.invoices.description":"Historial de pagos","billing.invoices.empty":"No se encontraron facturas","billing.invoices.paid":"Pagado","billing.processing.page_title":"Procesando...","billing.processing.title":"Procesando tu suscripción","billing.processing.description":"Espera mientras configuramos tu cuenta. Solo tomará un momento.","billing.processing.success_title":"¡Todo listo!","billing.processing.success_description":"Tu suscripción está activa. Redirigiendo a tus workspaces...","billing.processing.cancelled_title":"Pago cancelado","billing.processing.cancelled_description":"Tu pago fue cancelado. No se realizaron cargos.","billing.processing.retry":"Intentar de nuevo","billing.status.active":"Activa","billing.status.canceled":"Cancelada","billing.status.incomplete":"Incompleta","billing.status.incomplete_expired":"Expirada","billing.status.past_due":"Vencida","billing.status.trialing":"Prueba","billing.status.unpaid":"Sin pagar","calendar.title":"Calendario","calendar.today":"Hoy","calendar.day":"Día","calendar.week":"Semana","calendar.month":"Mes","calendar.new_post":"Nuevo post","calendar.no_content":"Sin contenido","calendar.more":"+:count más","common.confirm_modal.cannot_be_undone":"Esta acción no se puede deshacer.","common.confirm_modal.type":"Escribe","common.confirm_modal.to_confirm":"para confirmar.","common.confirm_modal.copy_to_clipboard":"Copiar al portapapeles","common.photo_upload.upload":"Subir","common.photo_upload.uploading":"Subiendo...","common.photo_upload.remove":"Eliminar foto","common.photo_upload.hint":"Recomendado: imagen cuadrada, máximo 2 MB.","common.timezone.select":"Seleccionar zona horaria","common.timezone.search":"Buscar zona horaria...","common.timezone.empty":"No se encontró zona horaria","common.date_picker.select":"Seleccionar fecha","common.cancel":"Cancelar","hashtags.title":"Hashtags","hashtags.description":"Crea grupos de hashtags para agregarlos rápidamente a tus posts","hashtags.search":"Buscar hashtags...","hashtags.new_group":"Nuevo grupo","hashtags.no_groups_yet":"Aún no hay grupos de hashtags","hashtags.no_groups_description":"Crea grupos de hashtags para agregar rápidamente hashtags populares a tus posts","hashtags.create_first_group":"Crea tu primer grupo","hashtags.hashtags_count":":count hashtags","hashtags.create.title":"Crear grupo de hashtags","hashtags.create.description":"Dale un nombre a tu grupo y agrega hashtags separados por espacios o comas","hashtags.create.name":"Nombre del grupo","hashtags.create.name_placeholder":"ej: Marketing, Viajes, Comida","hashtags.create.hashtags":"Hashtags","hashtags.create.hashtags_placeholder":"#marketing #redessociales #negocios #crecimiento","hashtags.create.hashtags_hint":"Ingresa hashtags separados por espacios o comas. Incluye el símbolo #.","hashtags.create.submit":"Crear grupo","hashtags.create.submitting":"Creando...","hashtags.edit.title":"Editar grupo de hashtags","hashtags.edit.description":"Actualiza el nombre y los hashtags de este grupo","hashtags.edit.name":"Nombre del grupo","hashtags.edit.name_placeholder":"ej: Marketing, Viajes, Comida","hashtags.edit.hashtags":"Hashtags","hashtags.edit.hashtags_placeholder":"#marketing #redessociales #negocios #crecimiento","hashtags.edit.hashtags_hint":"Ingresa hashtags separados por espacios o comas. Incluye el símbolo #.","hashtags.edit.submit":"Guardar cambios","hashtags.edit.submitting":"Guardando...","hashtags.delete.title":"Eliminar grupo de hashtags","hashtags.delete.description":"¿Estás seguro de que deseas eliminar este grupo de hashtags? Esta acción no se puede deshacer.","hashtags.delete.confirm":"Eliminar","hashtags.delete.cancel":"Cancelar","hashtags.flash.created":"¡Grupo de hashtags creado correctamente!","hashtags.flash.updated":"¡Grupo de hashtags actualizado correctamente!","hashtags.flash.deleted":"¡Grupo de hashtags eliminado correctamente!","labels.title":"Etiquetas","labels.description":"Crea etiquetas para organizar y categorizar tus posts","labels.search":"Buscar etiquetas...","labels.new_label":"Nueva etiqueta","labels.no_labels_yet":"Aún no hay etiquetas","labels.create_first_label":"Crea tu primera etiqueta","labels.create.title":"Crear etiqueta","labels.create.description":"Dale un nombre y elige un color para tu etiqueta","labels.create.name":"Nombre","labels.create.name_placeholder":"Nombre de la etiqueta...","labels.create.color":"Color","labels.create.submit":"Crear etiqueta","labels.create.submitting":"Creando...","labels.edit.title":"Editar etiqueta","labels.edit.description":"Actualiza el nombre y el color de esta etiqueta","labels.edit.name":"Nombre","labels.edit.name_placeholder":"Nombre de la etiqueta...","labels.edit.color":"Color","labels.edit.submit":"Guardar cambios","labels.edit.submitting":"Guardando...","labels.delete.title":"Eliminar etiqueta","labels.delete.description":"¿Estás seguro de que deseas eliminar esta etiqueta? Esta acción no se puede deshacer.","labels.delete.confirm":"Eliminar","labels.delete.cancel":"Cancelar","labels.flash.created":"¡Etiqueta creada correctamente!","labels.flash.updated":"¡Etiqueta actualizada correctamente!","labels.flash.deleted":"¡Etiqueta eliminada correctamente!","mail.workspace_connections_disconnected.subject":"{1} :count cuenta necesita ser reconectada en :workspace|[2,*] :count cuentas necesitan ser reconectadas en :workspace","mail.workspace_connections_disconnected.title":"Cuentas necesitan reconexión","mail.workspace_connections_disconnected.intro":"Las siguientes cuentas sociales en tu workspace :workspace se han desconectado y necesitan ser reconectadas:","mail.workspace_connections_disconnected.reasons_title":"Esto puede haber ocurrido porque:","mail.workspace_connections_disconnected.reason_expired":"Los tokens de acceso expiraron","mail.workspace_connections_disconnected.reason_revoked":"Revocaste el acceso a TryPost en la plataforma","mail.workspace_connections_disconnected.reason_changed":"La plataforma cambió sus requisitos de autenticación","mail.workspace_connections_disconnected.reconnect_cta":"Reconecta estas cuentas para seguir programando y publicando posts.","mail.workspace_connections_disconnected.button":"Reconectar cuentas","onboarding.role.page_title":"Bienvenido - Cuéntanos sobre ti","onboarding.role.title":"Cuéntanos sobre ti","onboarding.role.description":"Ayúdanos a personalizar tu experiencia","onboarding.role.submit":"Continuar","onboarding.connect.page_title":"Conecta tus cuentas","onboarding.connect.title":"Conecta tus cuentas","onboarding.connect.description":"Conecta al menos una red social para comenzar","onboarding.connect.submit":"Continuar","onboarding.connect.error":"Algo salió mal. Inténtalo de nuevo.","onboarding.connect.go_back":"Volver","pagination.previous":"« Anterior","pagination.next":"Siguiente »","passwords.reset":"Tu contraseña ha sido restablecida.","passwords.sent":"Te hemos enviado un enlace para restablecer tu contraseña.","passwords.throttled":"Espera antes de intentarlo de nuevo.","passwords.token":"Este token de restablecimiento de contraseña no es válido.","passwords.user":"No encontramos un usuario con ese correo electrónico.","posts.title":"Posts","posts.search":"Buscar posts...","posts.all_posts":"Todos los posts","posts.new_post":"Nuevo post","posts.no_posts":"No se encontraron posts","posts.start_creating":"Empieza creando tu primer post.","posts.manage_posts":"Administra todos tus posts","posts.delete_confirm":"¿Estás seguro de que deseas eliminar este post?","posts.by":"por","posts.actions.view":"Ver post","posts.actions.delete":"Eliminar post","posts.form.post_type":"Tipo de post","posts.form.board":"Tablero","posts.form.select_board":"Seleccionar tablero","posts.form.search_board":"Buscar tablero...","posts.form.no_board_found":"No se encontró tablero","posts.form.media":"Multimedia","posts.form.min":"Min","posts.form.uploading":"Subiendo...","posts.form.drop_to_upload":"Suelta para subir","posts.form.drag_and_drop":"Arrastra y suelta o haz clic para subir","posts.form.photos_and_videos":"Fotos y videos","posts.form.photos_only":"Solo fotos","posts.form.videos_only":"Solo videos","posts.form.drag_to_reorder":"Arrastra para reordenar","posts.form.caption":"Descripción","posts.form.write_caption":"Escribe tu descripción...","posts.status.pending":"Pendiente","posts.status.draft":"Borrador","posts.status.scheduled":"Programado","posts.status.publishing":"Publicando","posts.status.published":"Publicado","posts.status.partially_published":"Parcialmente publicado","posts.status.failed":"Fallido","posts.descriptions.draft":"Posts esperando ser programados","posts.descriptions.scheduled":"Posts programados para publicar","posts.descriptions.published":"Posts ya publicados","posts.edit.title":"Editar post","posts.edit.view_title":"Ver post","posts.edit.manage_platforms":"Administrar plataformas","posts.edit.sync":"Sincronizar","posts.edit.labels":"Etiquetas","posts.edit.hashtags":"Hashtags","posts.edit.schedule":"Programar","posts.edit.publish":"Publicar","posts.edit.delete":"Eliminar","posts.edit.settings":"Configuración","posts.edit.schedule_for":"Programar para","posts.edit.saving":"Guardando...","posts.edit.saved":"Guardado","posts.edit.scheduled_at":"Programado:","posts.edit.published_at":"Publicado:","posts.edit.media":"Multimedia","posts.edit.caption":"Descripción","posts.edit.no_caption":"Sin descripción","posts.edit.no_content":"Sin contenido","posts.edit.empty_state.title":"No hay plataformas seleccionadas","posts.edit.empty_state.description":"Selecciona al menos una plataforma para crear tu post","posts.edit.delete_modal.title":"Eliminar post","posts.edit.delete_modal.description":"¿Estás seguro de que deseas eliminar este post? Esta acción no se puede deshacer.","posts.edit.delete_modal.action":"Eliminar","posts.edit.delete_modal.cancel":"Cancelar","posts.edit.sync_enable.title":"¿Activar sincronización?","posts.edit.sync_enable.description":"Todas las plataformas compartirán el mismo contenido. Las ediciones personalizadas realizadas en plataformas individuales serán reemplazadas con el contenido actual.","posts.edit.sync_enable.cancel":"Cancelar","posts.edit.sync_enable.action":"Activar sincronización","posts.edit.sync_disable.title":"¿Desactivar sincronización?","posts.edit.sync_disable.description":"Cada plataforma mantendrá su contenido actual, pero las ediciones futuras solo se aplicarán a la plataforma que estés editando.","posts.edit.sync_disable.customize_note":"Podrás personalizar el contenido de cada plataforma individualmente.","posts.edit.sync_disable.cancel":"Cancelar","posts.edit.sync_disable.action":"Desactivar sincronización","posts.edit.platforms_dialog.title":"Seleccionar plataformas","posts.edit.platforms_dialog.description":"Elige en qué plataformas publicar este post.","posts.edit.hashtags_modal.search":"Buscar hashtags...","posts.edit.hashtags_modal.no_results":"No se encontraron hashtags.","posts.edit.validation.select_board":"Selecciona un tablero","posts.edit.validation.images_not_supported":"Imágenes no soportadas","posts.edit.validation.videos_not_supported":"Videos no soportados","posts.edit.validation.max_images":"Máximo :count imágenes","posts.edit.validation.requires_media":"Requiere multimedia","posts.edit.validation.exceeded":":count excedido","posts.edit.validation.does_not_support_images":":platform no soporta imágenes","posts.edit.validation.supports_up_to_images":":platform soporta hasta :count imágenes","posts.edit.validation.does_not_support_videos":":platform no soporta videos","posts.content_types.instagram_feed.label":"Post del feed","posts.content_types.instagram_feed.description":"Aparece en tu feed y perfil","posts.content_types.instagram_reel.label":"Reel","posts.content_types.instagram_reel.description":"Video corto de hasta 90 segundos","posts.content_types.instagram_story.label":"Historia","posts.content_types.instagram_story.description":"Desaparece después de 24 horas","posts.content_types.linkedin_post.label":"Post","posts.content_types.linkedin_post.description":"Post estándar con texto y multimedia","posts.content_types.linkedin_carousel.label":"Carrusel","posts.content_types.linkedin_carousel.description":"Imágenes deslizables","posts.content_types.linkedin_page_post.label":"Post","posts.content_types.linkedin_page_post.description":"Post estándar con texto y multimedia","posts.content_types.linkedin_page_carousel.label":"Carrusel","posts.content_types.linkedin_page_carousel.description":"Imágenes deslizables","posts.content_types.facebook_post.label":"Post","posts.content_types.facebook_post.description":"Post estándar en tu página","posts.content_types.facebook_reel.label":"Reel","posts.content_types.facebook_reel.description":"Video corto de hasta 90 segundos","posts.content_types.facebook_story.label":"Historia","posts.content_types.facebook_story.description":"Desaparece después de 24 horas","posts.content_types.tiktok_video.label":"Video","posts.content_types.tiktok_video.description":"Contenido de video corto","posts.content_types.youtube_short.label":"Short","posts.content_types.youtube_short.description":"Video vertical de hasta 60 segundos","posts.content_types.x_post.label":"Post","posts.content_types.x_post.description":"Tweet con texto y multimedia","posts.content_types.threads_post.label":"Post","posts.content_types.threads_post.description":"Post de texto con multimedia opcional","posts.content_types.pinterest_pin.label":"Pin","posts.content_types.pinterest_pin.description":"Pin de imagen con enlace","posts.content_types.pinterest_video_pin.label":"Pin de video","posts.content_types.pinterest_video_pin.description":"Contenido de video","posts.content_types.pinterest_carousel.label":"Carrusel","posts.content_types.pinterest_carousel.description":"2-5 imágenes","posts.content_types.bluesky_post.label":"Post","posts.content_types.bluesky_post.description":"Post de texto con imágenes opcionales","posts.content_types.mastodon_post.label":"Post","posts.content_types.mastodon_post.description":"Post de texto con multimedia opcional","posts.platforms.linkedin":"LinkedIn","posts.platforms.linkedin-page":"Página de LinkedIn","posts.platforms.x":"X","posts.platforms.tiktok":"TikTok","posts.platforms.youtube":"YouTube Shorts","posts.platforms.facebook":"Página de Facebook","posts.platforms.instagram":"Instagram","posts.platforms.threads":"Threads","posts.platforms.pinterest":"Pinterest","posts.platforms.bluesky":"Bluesky","posts.platforms.mastodon":"Mastodon","posts.flash.scheduled":"¡Post programado correctamente!","posts.flash.publishing":"¡El post se está publicando!","posts.flash.deleted":"¡Post eliminado correctamente!","posts.flash.cannot_edit_published":"Los posts publicados no se pueden editar.","posts.flash.connect_first":"Conecta al menos una red social antes de crear un post.","posts.errors.account_disconnected":"Cuenta social desconectada","posts.errors.account_inactive":"Cuenta social desactivada","settings.title":"Configuración","settings.description":"Administra tu perfil y configuración de la cuenta","settings.nav.profile":"Perfil","settings.nav.password":"Contraseña","settings.nav.workspace":"Workspace","settings.nav.members":"Miembros","settings.nav.notifications":"Notificaciones","settings.nav.billing":"Facturación","settings.notifications.title":"Preferencias de notificaciones","settings.notifications.heading":"Notificaciones por correo","settings.notifications.description":"Elige qué notificaciones por correo deseas recibir","settings.notifications.post_published":"Post publicado","settings.notifications.post_published_description":"Recibir un correo cuando tu post se publique correctamente","settings.notifications.post_failed":"Post fallido","settings.notifications.post_failed_description":"Recibir un correo cuando tu post falle al publicar","settings.notifications.account_disconnected":"Cuenta desconectada","settings.notifications.account_disconnected_description":"Recibir un correo cuando una cuenta social se desconecte","settings.notifications.save":"Guardar preferencias","settings.profile.title":"Configuración del perfil","settings.profile.photo_heading":"Foto de perfil","settings.profile.photo_description":"Sube una foto de perfil","settings.profile.heading":"Información del perfil","settings.profile.description":"Actualiza tu nombre y correo electrónico","settings.profile.avatar":"Avatar","settings.profile.name":"Nombre","settings.profile.name_placeholder":"Nombre completo","settings.profile.email":"Correo electrónico","settings.profile.email_placeholder":"Correo electrónico","settings.profile.email_unverified":"Tu correo electrónico no ha sido verificado.","settings.profile.resend_verification":"Haz clic aquí para reenviar el correo de verificación.","settings.profile.verification_sent":"Se ha enviado un nuevo enlace de verificación a tu correo electrónico.","settings.profile.save":"Guardar","settings.password.title":"Configuración de contraseña","settings.password.heading":"Actualizar contraseña","settings.password.description":"Asegúrate de que tu cuenta use una contraseña larga y aleatoria para mantenerte seguro","settings.password.current_password":"Contraseña actual","settings.password.current_password_placeholder":"Contraseña actual","settings.password.new_password":"Nueva contraseña","settings.password.new_password_placeholder":"Nueva contraseña","settings.password.confirm_password":"Confirmar contraseña","settings.password.confirm_password_placeholder":"Confirmar contraseña","settings.password.save":"Guardar contraseña","settings.delete_account.heading":"Eliminar cuenta","settings.delete_account.description":"Elimina tu cuenta y todos sus recursos","settings.delete_account.warning":"Advertencia","settings.delete_account.warning_message":"Procede con precaución, esta acción no se puede deshacer.","settings.delete_account.button":"Eliminar cuenta","settings.delete_account.modal_title":"¿Estás seguro de que deseas eliminar tu cuenta?","settings.delete_account.modal_description":"Una vez eliminada tu cuenta, todos sus recursos y datos también se eliminarán permanentemente. Introduce tu contraseña para confirmar que deseas eliminar permanentemente tu cuenta.","settings.delete_account.password":"Contraseña","settings.delete_account.password_placeholder":"Contraseña","settings.delete_account.cancel":"Cancelar","settings.delete_account.confirm":"Eliminar cuenta","settings.workspace.title":"Configuración del workspace","settings.workspace.logo_heading":"Logo del workspace","settings.workspace.logo_description":"Sube un logo para tu workspace","settings.workspace.heading":"Nombre del workspace","settings.workspace.description":"Actualiza el nombre y zona horaria del workspace","settings.workspace.members_heading":"Miembros","settings.workspace.members_description":"Administra miembros e invitaciones del workspace","settings.workspace.name":"Nombre","settings.workspace.name_placeholder":"Mi Workspace","settings.workspace.timezone":"Zona horaria","settings.workspace.save":"Guardar","settings.members.title":"Miembros","settings.members.heading":"Miembros del equipo","settings.members.description":"Administra miembros e invitaciones de este workspace","settings.members.cancel":"Cancelar","settings.members.remove":"Eliminar","settings.members.make_admin":"Hacer administrador","settings.members.make_member":"Hacer miembro","settings.members.invite.title":"Invitar miembro","settings.members.invite.description":"Envía una invitación por correo para agregar colaboradores","settings.members.invite.email":"Correo electrónico","settings.members.invite.email_placeholder":"colaborador@email.com","settings.members.invite.role":"Rol","settings.members.invite.role_placeholder":"Selecciona un rol","settings.members.invite.submit":"Enviar invitación","settings.members.pending.title":"Invitaciones pendientes","settings.members.pending.description":"Invitaciones en espera de aceptación","settings.members.pending.empty":"No hay invitaciones pendientes","settings.members.list.title":"Miembros","settings.members.list.description":"Personas con acceso a este workspace","settings.members.list.empty":"No hay miembros además del propietario","settings.members.remove_modal.title":"Eliminar miembro","settings.members.remove_modal.description":"¿Estás seguro de que deseas eliminar a este miembro del workspace? Perderá acceso a todos los recursos del workspace.","settings.members.remove_modal.action":"Eliminar miembro","settings.members.cancel_invite_modal.title":"Cancelar invitación","settings.members.cancel_invite_modal.description":"¿Estás seguro de que deseas cancelar esta invitación?","settings.members.cancel_invite_modal.action":"Cancelar invitación","settings.members.roles.owner":"Propietario","settings.members.roles.admin":"Administrador","settings.members.roles.member":"Miembro","settings.members.flash.invite_sent":"¡Invitación enviada correctamente!","settings.members.flash.invite_deleted":"Invitación eliminada.","settings.members.flash.member_removed":"¡Miembro eliminado correctamente!","settings.members.flash.role_updated":"Rol del miembro actualizado.","settings.members.flash.wrong_email":"Esta invitación es para otro correo electrónico.","settings.members.flash.already_member":"Ya eres miembro de este workspace.","settings.members.flash.invite_accepted":"¡Bienvenido! Ahora eres miembro del workspace.","settings.members.flash.invite_declined":"Invitación rechazada.","settings.flash.profile_updated":"¡Perfil actualizado correctamente!","settings.flash.language_updated":"¡Idioma actualizado correctamente!","settings.flash.password_updated":"¡Contraseña actualizada correctamente!","settings.flash.workspace_updated":"¡Configuración actualizada correctamente!","settings.flash.photo_updated":"¡Foto actualizada correctamente!","settings.flash.photo_deleted":"¡Foto eliminada correctamente!","settings.flash.logo_updated":"¡Logo subido correctamente!","settings.flash.logo_deleted":"¡Logo eliminado correctamente!","settings.flash.notifications_updated":"¡Preferencias de notificaciones actualizadas!","settings.api_keys.title":"Claves API","settings.api_keys.page_title":"Claves API","settings.api_keys.heading":"Claves API","settings.api_keys.description":"Administra claves API para acceso programático a tu workspace.","settings.api_keys.create":"Crear clave API","settings.api_keys.copy":"Copiar","settings.api_keys.new_token_message":"Tu nueva clave API ha sido creada. Cópiala ahora — no podrás verla de nuevo.","settings.api_keys.table.name":"Nombre","settings.api_keys.table.key":"Clave","settings.api_keys.table.status":"Estado","settings.api_keys.table.expires":"Expira","settings.api_keys.table.last_used":"Último uso","settings.api_keys.table.never":"Nunca","settings.api_keys.actions.copy_id":"Copiar ID de clave API","settings.api_keys.actions.copy_id_success":"ID de clave API copiado","settings.api_keys.actions.delete":"Eliminar","settings.api_keys.empty.title":"No hay claves API","settings.api_keys.empty.description":"Crea una clave API para acceder a tu workspace programáticamente.","settings.api_keys.delete_modal.title":"Eliminar clave API","settings.api_keys.delete_modal.description":"¿Estás seguro de que deseas eliminar esta clave API? Las aplicaciones que la usen perderán acceso inmediatamente.","settings.api_keys.delete_modal.action":"Eliminar clave API","settings.api_keys.create_dialog.title":"Crear clave API","settings.api_keys.create_dialog.description":"Crea una nueva clave API para acceso programático a tu workspace.","settings.api_keys.create_dialog.name":"Nombre","settings.api_keys.create_dialog.name_placeholder":"ej. Clave API de Producción","settings.api_keys.create_dialog.expires":"Fecha de expiración (opcional)","settings.api_keys.create_dialog.expires_placeholder":"Sin expiración","settings.api_keys.create_dialog.submit":"Crear","settings.api_keys.create_dialog.cancel":"Cancelar","settings.api_keys.flash.created":"¡Clave API creada correctamente!","settings.api_keys.flash.deleted":"¡Clave API eliminada correctamente!","sidebar.workspaces":"Workspaces","sidebar.select_workspace":"Seleccionar workspace","sidebar.create_workspace":"Crear workspace","sidebar.create_post":"Crear post","sidebar.profile":"Perfil","sidebar.account":"Cuenta","sidebar.log_out":"Cerrar sesión","sidebar.workspace":"Workspace: :name","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.configuration":"Configuración","sidebar.groups.support":"Soporte","sidebar.posts.calendar":"Calendario","sidebar.posts.all":"Todos","sidebar.posts.scheduled":"Programados","sidebar.posts.posted":"Publicados","sidebar.posts.drafts":"Borradores","sidebar.config.connections":"Conexiones","sidebar.config.hashtags":"Hashtags","sidebar.config.labels":"Etiquetas","sidebar.config.settings":"Configuración","sidebar.notifications":"Notificaciones","sidebar.mark_all_read":"Marcar todo como leído","sidebar.mark_as_read":"Marcar como leído","sidebar.archive_all":"Archivar todo","sidebar.no_notifications":"Sin notificaciones","sidebar.support.discord":"Discord","sidebar.support.share_feedback":"Dar feedback","sidebar.support.last_updates":"Últimas actualizaciones","sidebar.support.docs":"Documentación","validation.accepted":"El campo :attribute debe ser aceptado.","validation.accepted_if":"El campo :attribute debe ser aceptado cuando :other es :value.","validation.active_url":"El campo :attribute debe ser una URL válida.","validation.after":"El campo :attribute debe ser una fecha posterior a :date.","validation.after_or_equal":"El campo :attribute debe ser una fecha posterior o igual a :date.","validation.alpha":"El campo :attribute solo puede contener letras.","validation.alpha_dash":"El campo :attribute solo puede contener letras, números, guiones y guiones bajos.","validation.alpha_num":"El campo :attribute solo puede contener letras y números.","validation.any_of":"El campo :attribute no es válido.","validation.array":"El campo :attribute debe ser un arreglo.","validation.ascii":"El campo :attribute solo puede contener caracteres alfanuméricos de un byte y símbolos.","validation.before":"El campo :attribute debe ser una fecha anterior a :date.","validation.before_or_equal":"El campo :attribute debe ser una fecha anterior o igual a :date.","validation.between.array":"El campo :attribute debe tener entre :min y :max elementos.","validation.between.file":"El campo :attribute debe pesar entre :min y :max kilobytes.","validation.between.numeric":"El campo :attribute debe estar entre :min y :max.","validation.between.string":"El campo :attribute debe tener entre :min y :max caracteres.","validation.boolean":"El campo :attribute debe ser verdadero o falso.","validation.can":"El campo :attribute contiene un valor no autorizado.","validation.confirmed":"La confirmación del campo :attribute no coincide.","validation.contains":"Al campo :attribute le falta un valor requerido.","validation.current_password":"La contraseña es incorrecta.","validation.date":"El campo :attribute debe ser una fecha válida.","validation.date_equals":"El campo :attribute debe ser una fecha igual a :date.","validation.date_format":"El campo :attribute debe coincidir con el formato :format.","validation.decimal":"El campo :attribute debe tener :decimal decimales.","validation.declined":"El campo :attribute debe ser rechazado.","validation.declined_if":"El campo :attribute debe ser rechazado cuando :other es :value.","validation.different":"El campo :attribute y :other deben ser diferentes.","validation.digits":"El campo :attribute debe tener :digits dígitos.","validation.digits_between":"El campo :attribute debe tener entre :min y :max dígitos.","validation.dimensions":"El campo :attribute tiene dimensiones de imagen no válidas.","validation.distinct":"El campo :attribute tiene un valor duplicado.","validation.doesnt_contain":"El campo :attribute no debe contener ninguno de los siguientes: :values.","validation.doesnt_end_with":"El campo :attribute no debe terminar con uno de los siguientes: :values.","validation.doesnt_start_with":"El campo :attribute no debe comenzar con uno de los siguientes: :values.","validation.email":"El campo :attribute debe ser un correo electrónico válido.","validation.encoding":"El campo :attribute debe estar codificado en :encoding.","validation.ends_with":"El campo :attribute debe terminar con uno de los siguientes: :values.","validation.enum":"El :attribute seleccionado no es válido.","validation.exists":"El :attribute seleccionado no es válido.","validation.extensions":"El campo :attribute debe tener una de las siguientes extensiones: :values.","validation.file":"El campo :attribute debe ser un archivo.","validation.filled":"El campo :attribute debe tener un valor.","validation.gt.array":"El campo :attribute debe tener más de :value elementos.","validation.gt.file":"El campo :attribute debe pesar más de :value kilobytes.","validation.gt.numeric":"El campo :attribute debe ser mayor que :value.","validation.gt.string":"El campo :attribute debe tener más de :value caracteres.","validation.gte.array":"El campo :attribute debe tener :value elementos o más.","validation.gte.file":"El campo :attribute debe pesar :value kilobytes o más.","validation.gte.numeric":"El campo :attribute debe ser mayor o igual a :value.","validation.gte.string":"El campo :attribute debe tener :value caracteres o más.","validation.hex_color":"El campo :attribute debe ser un color hexadecimal válido.","validation.image":"El campo :attribute debe ser una imagen.","validation.in":"El :attribute seleccionado no es válido.","validation.in_array":"El campo :attribute debe existir en :other.","validation.in_array_keys":"El campo :attribute debe contener al menos una de las siguientes claves: :values.","validation.integer":"El campo :attribute debe ser un número entero.","validation.ip":"El campo :attribute debe ser una dirección IP válida.","validation.ipv4":"El campo :attribute debe ser una dirección IPv4 válida.","validation.ipv6":"El campo :attribute debe ser una dirección IPv6 válida.","validation.json":"El campo :attribute debe ser una cadena JSON válida.","validation.list":"El campo :attribute debe ser una lista.","validation.lowercase":"El campo :attribute debe estar en minúsculas.","validation.lt.array":"El campo :attribute debe tener menos de :value elementos.","validation.lt.file":"El campo :attribute debe pesar menos de :value kilobytes.","validation.lt.numeric":"El campo :attribute debe ser menor que :value.","validation.lt.string":"El campo :attribute debe tener menos de :value caracteres.","validation.lte.array":"El campo :attribute no debe tener más de :value elementos.","validation.lte.file":"El campo :attribute debe pesar :value kilobytes o menos.","validation.lte.numeric":"El campo :attribute debe ser menor o igual a :value.","validation.lte.string":"El campo :attribute debe tener :value caracteres o menos.","validation.mac_address":"El campo :attribute debe ser una dirección MAC válida.","validation.max.array":"El campo :attribute no debe tener más de :max elementos.","validation.max.file":"El campo :attribute no debe pesar más de :max kilobytes.","validation.max.numeric":"El campo :attribute no debe ser mayor que :max.","validation.max.string":"El campo :attribute no debe tener más de :max caracteres.","validation.max_digits":"El campo :attribute no debe tener más de :max dígitos.","validation.mimes":"El campo :attribute debe ser un archivo de tipo: :values.","validation.mimetypes":"El campo :attribute debe ser un archivo de tipo: :values.","validation.min.array":"El campo :attribute debe tener al menos :min elementos.","validation.min.file":"El campo :attribute debe pesar al menos :min kilobytes.","validation.min.numeric":"El campo :attribute debe ser al menos :min.","validation.min.string":"El campo :attribute debe tener al menos :min caracteres.","validation.min_digits":"El campo :attribute debe tener al menos :min dígitos.","validation.missing":"El campo :attribute debe estar ausente.","validation.missing_if":"El campo :attribute debe estar ausente cuando :other es :value.","validation.missing_unless":"El campo :attribute debe estar ausente a menos que :other sea :value.","validation.missing_with":"El campo :attribute debe estar ausente cuando :values está presente.","validation.missing_with_all":"El campo :attribute debe estar ausente cuando :values están presentes.","validation.multiple_of":"El campo :attribute debe ser múltiplo de :value.","validation.not_in":"El :attribute seleccionado no es válido.","validation.not_regex":"El formato del campo :attribute no es válido.","validation.numeric":"El campo :attribute debe ser un número.","validation.password.letters":"El campo :attribute debe contener al menos una letra.","validation.password.mixed":"El campo :attribute debe contener al menos una letra mayúscula y una minúscula.","validation.password.numbers":"El campo :attribute debe contener al menos un número.","validation.password.symbols":"El campo :attribute debe contener al menos un símbolo.","validation.password.uncompromised":"El :attribute proporcionado ha aparecido en una filtración de datos. Elige un :attribute diferente.","validation.present":"El campo :attribute debe estar presente.","validation.present_if":"El campo :attribute debe estar presente cuando :other es :value.","validation.present_unless":"El campo :attribute debe estar presente a menos que :other sea :value.","validation.present_with":"El campo :attribute debe estar presente cuando :values está presente.","validation.present_with_all":"El campo :attribute debe estar presente cuando :values están presentes.","validation.prohibited":"El campo :attribute está prohibido.","validation.prohibited_if":"El campo :attribute está prohibido cuando :other es :value.","validation.prohibited_if_accepted":"El campo :attribute está prohibido cuando :other es aceptado.","validation.prohibited_if_declined":"El campo :attribute está prohibido cuando :other es rechazado.","validation.prohibited_unless":"El campo :attribute está prohibido a menos que :other esté en :values.","validation.prohibits":"El campo :attribute prohíbe que :other esté presente.","validation.regex":"El formato del campo :attribute no es válido.","validation.required":"El campo :attribute es obligatorio.","validation.required_array_keys":"El campo :attribute debe contener entradas para: :values.","validation.required_if":"El campo :attribute es obligatorio cuando :other es :value.","validation.required_if_accepted":"El campo :attribute es obligatorio cuando :other es aceptado.","validation.required_if_declined":"El campo :attribute es obligatorio cuando :other es rechazado.","validation.required_unless":"El campo :attribute es obligatorio a menos que :other esté en :values.","validation.required_with":"El campo :attribute es obligatorio cuando :values está presente.","validation.required_with_all":"El campo :attribute es obligatorio cuando :values están presentes.","validation.required_without":"El campo :attribute es obligatorio cuando :values no está presente.","validation.required_without_all":"El campo :attribute es obligatorio cuando ninguno de :values está presente.","validation.same":"El campo :attribute debe coincidir con :other.","validation.size.array":"El campo :attribute debe contener :size elementos.","validation.size.file":"El campo :attribute debe pesar :size kilobytes.","validation.size.numeric":"El campo :attribute debe ser :size.","validation.size.string":"El campo :attribute debe tener :size caracteres.","validation.starts_with":"El campo :attribute debe comenzar con uno de los siguientes: :values.","validation.string":"El campo :attribute debe ser una cadena de texto.","validation.timezone":"El campo :attribute debe ser una zona horaria válida.","validation.unique":"El :attribute ya ha sido registrado.","validation.uploaded":"El :attribute no se pudo subir.","validation.uppercase":"El campo :attribute debe estar en mayúsculas.","validation.url":"El campo :attribute debe ser una URL válida.","validation.ulid":"El campo :attribute debe ser un ULID válido.","validation.uuid":"El campo :attribute debe ser un UUID válido.","validation.custom.attribute-name.rule-name":"custom-message","workspaces.title":"Workspaces","workspaces.select_title":"Tus workspaces","workspaces.select_description":"Selecciona un workspace para continuar","workspaces.current":"Actual","workspaces.connections":":count conexiones","workspaces.posts":":count posts","workspaces.create.title":"Crear un workspace","workspaces.create.description":"Dale un nombre a tu workspace para comenzar","workspaces.create.name":"Nombre del workspace","workspaces.create.name_placeholder":"Mi Workspace","workspaces.create.submit":"Crear workspace"} \ No newline at end of file diff --git a/lang/php_pt-BR.json b/lang/php_pt-BR.json new file mode 100644 index 00000000..73400fe8 --- /dev/null +++ b/lang/php_pt-BR.json @@ -0,0 +1 @@ +{"accounts.title":"Conexões","accounts.page_title":"Contas Conectadas","accounts.description":"Conecte suas redes sociais para agendar e publicar posts","accounts.not_connected":"Não conectado","accounts.connect":"Conectar","accounts.connection_lost":"Conexão perdida","accounts.reconnect_account":"Reconectar conta","accounts.view_profile":"Ver perfil","accounts.disconnect":"Desconectar","accounts.disconnect_modal.title":"Desconectar Conta","accounts.disconnect_modal.description":"Tem certeza que deseja desconectar esta conta? Você pode reconectá-la a qualquer momento.","accounts.disconnect_modal.confirm":"Desconectar","accounts.disconnect_modal.cancel":"Cancelar","accounts.bluesky.title":"Conectar Bluesky","accounts.bluesky.description":"Digite suas credenciais para conectar","accounts.bluesky.email":"E-mail","accounts.bluesky.email_placeholder":"seuhandle.bsky.social","accounts.bluesky.app_password":"Senha do App","accounts.bluesky.app_password_placeholder":"xxxx-xxxx-xxxx-xxxx","accounts.bluesky.app_password_hint":"Use uma Senha do App por segurança. Crie uma em bsky.app/settings.","accounts.bluesky.submit":"Conectar Bluesky","accounts.bluesky.submitting":"Conectando...","accounts.mastodon.title":"Conectar Mastodon","accounts.mastodon.description":"Digite a instância do seu Mastodon","accounts.mastodon.instance_url":"URL da Instância","accounts.mastodon.instance_placeholder":"https://mastodon.social","accounts.mastodon.instance_hint":"Digite a URL da sua instância Mastodon (ex: mastodon.social, techhub.social)","accounts.mastodon.submit":"Continuar com Mastodon","accounts.mastodon.submitting":"Conectando...","accounts.facebook.title":"Selecionar Página do Facebook","accounts.facebook.description":"Escolha qual página você deseja conectar","accounts.facebook.no_pages":"Nenhuma página encontrada","accounts.facebook.no_pages_description":"Você não é administrador de nenhuma página do Facebook.","accounts.facebook.page_label":"Página do Facebook","accounts.linkedin.title":"Selecionar Página do LinkedIn","accounts.linkedin.description":"Escolha qual página você deseja conectar","accounts.linkedin.no_pages":"Nenhuma página encontrada","accounts.linkedin.no_pages_description":"Você não é administrador de nenhuma página do LinkedIn.","accounts.linkedin.page_label":"Página do LinkedIn","accounts.flash.disconnected":"Conta desconectada com sucesso!","accounts.flash.connected":"Conta conectada com sucesso!","accounts.flash.session_expired":"Sessão expirada. Por favor, tente novamente.","accounts.flash.workspace_not_found":"Workspace não encontrado.","accounts.flash.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.","auth.failed":"Essas credenciais não correspondem aos nossos registros.","auth.password":"A senha fornecida está incorreta.","auth.throttle":"Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.","auth.flash.welcome":"Bem-vindo ao TryPost!","auth.flash.welcome_trial":"Bem-vindo ao TryPost! Seu período de teste começou.","auth.legal":"Ao continuar, você concorda com nossos Termos de Serviço e Política de Privacidade.","auth.slides.calendar.title":"Calendário Visual","auth.slides.calendar.description":"Planeje e agende seu conteúdo com um calendário intuitivo de arrastar e soltar em todas as suas contas sociais.","auth.slides.scheduling.title":"Agendamento Inteligente","auth.slides.scheduling.description":"Agende posts no LinkedIn, X, Instagram, TikTok, YouTube e mais — tudo em um só lugar.","auth.slides.media.title":"Mídia Rica","auth.slides.media.description":"Publique imagens, carrosséis, stories e reels. Cada plataforma recebe o formato correto automaticamente.","auth.slides.video.title":"Publicação de Vídeo","auth.slides.video.description":"Envie vídeos uma vez e publique no TikTok, YouTube Shorts, Instagram Reels e Facebook Reels.","auth.slides.team.title":"Workspaces em Equipe","auth.slides.team.description":"Convide sua equipe, atribua funções e gerencie múltiplas marcas em workspaces separados.","auth.slides.hashtags.title":"Grupos de Hashtags","auth.slides.hashtags.description":"Salve coleções de hashtags e adicione aos posts com um clique. Organize com etiquetas e filtros.","auth.or_continue_with":"Ou continue com","auth.google_login":"Entrar com Google","auth.google_signup":"Cadastrar com Google","auth.signup_success.page_title":"Bem-vindo","auth.signup_success.title":"Configurando sua conta","auth.signup_success.description":"Isso geralmente leva apenas alguns segundos...","auth.login.title":"Entrar na sua conta","auth.login.description":"Digite seu email e senha abaixo para entrar","auth.login.page_title":"Entrar","auth.login.email":"Endereço de email","auth.login.password":"Senha","auth.login.forgot_password":"Esqueceu a senha?","auth.login.remember_me":"Lembrar de mim","auth.login.submit":"Entrar","auth.login.no_account":"Não tem uma conta?","auth.login.sign_up":"Cadastre-se","auth.register.title":"Criar uma conta","auth.register.description":"Digite seus dados abaixo para criar sua conta","auth.register.page_title":"Cadastro","auth.register.name":"Nome","auth.register.name_placeholder":"Nome completo","auth.register.email":"Endereço de email","auth.register.password":"Senha","auth.register.show_password":"Mostrar senha","auth.register.hide_password":"Esconder senha","auth.register.submit":"Criar conta","auth.register.has_account":"Já tem uma conta?","auth.register.log_in":"Entrar","auth.forgot_password.title":"Esqueceu a senha","auth.forgot_password.description":"Digite seu email para receber um link de redefinição de senha","auth.forgot_password.page_title":"Esqueceu a senha","auth.forgot_password.email":"Endereço de email","auth.forgot_password.submit":"Enviar link de redefinição","auth.forgot_password.return_to":"Ou, volte para","auth.forgot_password.log_in":"entrar","auth.reset_password.title":"Redefinir senha","auth.reset_password.description":"Por favor, digite sua nova senha abaixo","auth.reset_password.page_title":"Redefinir senha","auth.reset_password.email":"Email","auth.reset_password.password":"Senha","auth.reset_password.confirm_password":"Confirmar Senha","auth.reset_password.confirm_placeholder":"Confirmar senha","auth.reset_password.submit":"Redefinir senha","auth.verify_email.title":"Verificar email","auth.verify_email.description":"Por favor, verifique seu endereço de email clicando no link que acabamos de enviar.","auth.verify_email.page_title":"Verificação de email","auth.verify_email.link_sent":"Um novo link de verificação foi enviado para o endereço de email que você forneceu durante o cadastro.","auth.verify_email.resend":"Reenviar email de verificação","auth.verify_email.log_out":"Sair","auth.accept_invite.page_title":"Aceitar Convite","auth.accept_invite.title":"Você foi convidado!","auth.accept_invite.description":"Você foi convidado para participar do workspace :workspace.","auth.accept_invite.workspace":"Workspace","auth.accept_invite.your_role":"Seu cargo","auth.accept_invite.email":"Email","auth.accept_invite.accept":"Aceitar Convite","auth.accept_invite.decline":"Recusar Convite","auth.accept_invite.login_prompt":"Entre ou crie uma conta para aceitar este convite.","auth.accept_invite.log_in":"Entrar","auth.accept_invite.create_account":"Criar Conta","billing.title":"Assinatura","billing.description":"Gerencie sua assinatura e método de pagamento","billing.subscribe.page_title":"Comece seu teste gratuito","billing.subscribe.title":"Comece seu teste gratuito","billing.subscribe.description":":days dias grátis. Sem cartão de crédito para começar.","billing.subscribe.start_trial":"Começar teste de :days dias","billing.subscribe.cancel_anytime":"Cancele a qualquer momento. Sem perguntas.","billing.subscribe.switch_workspace":"Trocar workspace","billing.subscribe.features.calendar":"Calendário visual com arrastar e soltar","billing.subscribe.features.scheduling":"Agendamento ilimitado de posts","billing.subscribe.features.media":"Imagens, carrosséis e stories","billing.subscribe.features.video":"Publicação de vídeos em todas as plataformas","billing.subscribe.features.team":"Colaboração em equipe e workspaces","billing.subscribe.features.hashtags":"Grupos de hashtags e etiquetas","billing.trial.title":"Período de teste ativo","billing.trial.description":"Seu período de teste termina em :date. Após isso, sua assinatura será cobrada automaticamente.","billing.subscription.title":"Sua Assinatura","billing.subscription.workspaces":"Workspaces","billing.subscription.quantity":"Quantidade da assinatura","billing.subscription.expires":"Expira em :date","billing.subscription.canceled_on":"Sua assinatura será cancelada em :date","billing.subscription.manage":"Gerenciar no Stripe","billing.invoices.title":"Faturas","billing.invoices.description":"Histórico de pagamentos","billing.invoices.empty":"Nenhuma fatura encontrada","billing.invoices.paid":"Pago","billing.processing.page_title":"Processando...","billing.processing.title":"Processando sua assinatura","billing.processing.description":"Aguarde enquanto configuramos sua conta. Isso levará apenas um momento.","billing.processing.success_title":"Tudo pronto!","billing.processing.success_description":"Sua assinatura está ativa. Redirecionando para seus workspaces...","billing.processing.cancelled_title":"Pagamento cancelado","billing.processing.cancelled_description":"Seu pagamento foi cancelado. Nenhuma cobrança foi realizada.","billing.processing.retry":"Tentar novamente","billing.status.active":"Ativo","billing.status.canceled":"Cancelado","billing.status.incomplete":"Incompleto","billing.status.incomplete_expired":"Expirado","billing.status.past_due":"Vencido","billing.status.trialing":"Teste","billing.status.unpaid":"Não pago","calendar.title":"Calendário","calendar.today":"Hoje","calendar.day":"Dia","calendar.week":"Semana","calendar.month":"Mês","calendar.new_post":"Novo Post","calendar.no_content":"Sem conteúdo","calendar.more":"+:count mais","common.confirm_modal.cannot_be_undone":"Esta ação não pode ser desfeita.","common.confirm_modal.type":"Digite","common.confirm_modal.to_confirm":"para confirmar.","common.confirm_modal.copy_to_clipboard":"Copiar para a área de transferência","common.photo_upload.upload":"Enviar","common.photo_upload.uploading":"Enviando...","common.photo_upload.remove":"Remover foto","common.photo_upload.hint":"Recomendado: imagem quadrada, máximo 2 MB.","common.timezone.select":"Selecionar fuso horário","common.timezone.search":"Buscar fuso horário...","common.timezone.empty":"Fuso horário não encontrado","common.date_picker.select":"Selecionar data","common.cancel":"Cancelar","hashtags.title":"Hashtags","hashtags.description":"Crie grupos de hashtags para adicionar rapidamente aos seus posts","hashtags.search":"Buscar hashtags...","hashtags.new_group":"Novo Grupo","hashtags.no_groups_yet":"Nenhum grupo de hashtags ainda","hashtags.no_groups_description":"Crie grupos de hashtags para adicionar rapidamente hashtags populares aos seus posts","hashtags.create_first_group":"Crie seu primeiro grupo","hashtags.hashtags_count":":count hashtags","hashtags.create.title":"Criar Grupo de Hashtags","hashtags.create.description":"Dê um nome ao grupo e adicione hashtags separadas por espaços ou vírgulas","hashtags.create.name":"Nome do Grupo","hashtags.create.name_placeholder":"ex: Marketing, Viagem, Comida","hashtags.create.hashtags":"Hashtags","hashtags.create.hashtags_placeholder":"#marketing #redessociais #negocios #crescimento","hashtags.create.hashtags_hint":"Digite as hashtags separadas por espaços ou vírgulas. Inclua o símbolo #.","hashtags.create.submit":"Criar Grupo","hashtags.create.submitting":"Criando...","hashtags.edit.title":"Editar Grupo de Hashtags","hashtags.edit.description":"Atualize o nome e as hashtags deste grupo","hashtags.edit.name":"Nome do Grupo","hashtags.edit.name_placeholder":"ex: Marketing, Viagem, Comida","hashtags.edit.hashtags":"Hashtags","hashtags.edit.hashtags_placeholder":"#marketing #redessociais #negocios #crescimento","hashtags.edit.hashtags_hint":"Digite as hashtags separadas por espaços ou vírgulas. Inclua o símbolo #.","hashtags.edit.submit":"Salvar Alterações","hashtags.edit.submitting":"Salvando...","hashtags.delete.title":"Excluir Grupo de Hashtags","hashtags.delete.description":"Tem certeza que deseja excluir este grupo de hashtags? Esta ação não pode ser desfeita.","hashtags.delete.confirm":"Excluir","hashtags.delete.cancel":"Cancelar","hashtags.flash.created":"Grupo de hashtags criado com sucesso!","hashtags.flash.updated":"Grupo de hashtags atualizado com sucesso!","hashtags.flash.deleted":"Grupo de hashtags excluído com sucesso!","labels.title":"Etiquetas","labels.description":"Crie etiquetas para organizar e categorizar seus posts","labels.search":"Buscar etiquetas...","labels.new_label":"Nova Etiqueta","labels.no_labels_yet":"Nenhuma etiqueta ainda","labels.create_first_label":"Crie sua primeira etiqueta","labels.create.title":"Criar Etiqueta","labels.create.description":"Dê um nome e escolha uma cor para sua etiqueta","labels.create.name":"Nome","labels.create.name_placeholder":"Digite o nome da etiqueta...","labels.create.color":"Cor","labels.create.submit":"Criar Etiqueta","labels.create.submitting":"Criando...","labels.edit.title":"Editar Etiqueta","labels.edit.description":"Atualize o nome e a cor desta etiqueta","labels.edit.name":"Nome","labels.edit.name_placeholder":"Digite o nome da etiqueta...","labels.edit.color":"Cor","labels.edit.submit":"Salvar Alterações","labels.edit.submitting":"Salvando...","labels.delete.title":"Excluir Etiqueta","labels.delete.description":"Tem certeza que deseja excluir esta etiqueta? Esta ação não pode ser desfeita.","labels.delete.confirm":"Excluir","labels.delete.cancel":"Cancelar","labels.flash.created":"Etiqueta criada com sucesso!","labels.flash.updated":"Etiqueta atualizada com sucesso!","labels.flash.deleted":"Etiqueta excluída com sucesso!","mail.workspace_connections_disconnected.subject":"{1} :count conta precisa ser reconectada em :workspace|[2,*] :count contas precisam ser reconectadas em :workspace","mail.workspace_connections_disconnected.title":"Contas Precisam ser Reconectadas","mail.workspace_connections_disconnected.intro":"As seguintes contas de redes sociais no seu workspace :workspace foram desconectadas e precisam ser reconectadas:","mail.workspace_connections_disconnected.reasons_title":"Isso pode ter acontecido porque:","mail.workspace_connections_disconnected.reason_expired":"Os tokens de acesso expiraram","mail.workspace_connections_disconnected.reason_revoked":"Você revogou o acesso ao TryPost na plataforma","mail.workspace_connections_disconnected.reason_changed":"A plataforma mudou os requisitos de autenticação","mail.workspace_connections_disconnected.reconnect_cta":"Por favor, reconecte essas contas para continuar agendando e publicando posts.","mail.workspace_connections_disconnected.button":"Reconectar Contas","onboarding.role.page_title":"Bem-vindo - Conte-nos sobre você","onboarding.role.title":"Conte-nos sobre você","onboarding.role.description":"Nos ajude a personalizar sua experiência","onboarding.role.submit":"Continuar","onboarding.connect.page_title":"Conecte suas contas","onboarding.connect.title":"Conecte suas contas","onboarding.connect.description":"Conecte pelo menos uma rede social para começar","onboarding.connect.submit":"Continuar","onboarding.connect.error":"Algo deu errado. Tente novamente.","onboarding.connect.go_back":"Voltar","pagination.previous":"« Anterior","pagination.next":"Próximo »","passwords.reset":"Sua senha foi redefinida.","passwords.sent":"Enviamos o link de redefinição de senha por e-mail.","passwords.throttled":"Por favor, aguarde antes de tentar novamente.","passwords.token":"Este token de redefinição de senha é inválido.","passwords.user":"Não conseguimos encontrar um usuário com esse endereço de e-mail.","posts.title":"Posts","posts.search":"Buscar posts...","posts.all_posts":"Todos os Posts","posts.new_post":"Novo Post","posts.no_posts":"Nenhum post encontrado","posts.start_creating":"Comece criando seu primeiro post.","posts.manage_posts":"Gerencie todos os seus posts","posts.delete_confirm":"Tem certeza que deseja excluir este post?","posts.by":"por","posts.actions.view":"Ver post","posts.actions.delete":"Excluir post","posts.form.post_type":"Tipo de Post","posts.form.board":"Pasta","posts.form.select_board":"Selecione uma pasta","posts.form.search_board":"Buscar pasta...","posts.form.no_board_found":"Nenhuma pasta encontrada","posts.form.media":"Mídia","posts.form.min":"Mín","posts.form.uploading":"Enviando...","posts.form.drop_to_upload":"Solte para enviar","posts.form.drag_and_drop":"Arraste e solte ou clique para enviar","posts.form.photos_and_videos":"Fotos e vídeos","posts.form.photos_only":"Apenas fotos","posts.form.videos_only":"Apenas vídeos","posts.form.drag_to_reorder":"Arraste para reordenar","posts.form.caption":"Legenda","posts.form.write_caption":"Escreva sua legenda...","posts.status.pending":"Pendente","posts.status.draft":"Rascunho","posts.status.scheduled":"Agendado","posts.status.publishing":"Publicando","posts.status.published":"Publicado","posts.status.partially_published":"Parcialmente Publicado","posts.status.failed":"Falhou","posts.descriptions.draft":"Posts aguardando agendamento","posts.descriptions.scheduled":"Posts agendados para publicação","posts.descriptions.published":"Posts já publicados","posts.edit.title":"Editar Post","posts.edit.view_title":"Visualizar Post","posts.edit.manage_platforms":"Gerenciar plataformas","posts.edit.sync":"Sincronizar","posts.edit.labels":"Etiqueta","posts.edit.hashtags":"Hashtags","posts.edit.schedule":"Agendar","posts.edit.publish":"Publicar","posts.edit.delete":"Excluir","posts.edit.settings":"Configurações","posts.edit.schedule_for":"Agendar para","posts.edit.saving":"Salvando...","posts.edit.saved":"Salvo","posts.edit.scheduled_at":"Agendado:","posts.edit.published_at":"Publicado:","posts.edit.media":"Mídia","posts.edit.caption":"Legenda","posts.edit.no_caption":"Sem legenda","posts.edit.no_content":"Sem conteúdo","posts.edit.empty_state.title":"Nenhuma plataforma selecionada","posts.edit.empty_state.description":"Selecione pelo menos uma plataforma para criar seu post","posts.edit.delete_modal.title":"Excluir Post","posts.edit.delete_modal.description":"Tem certeza que deseja excluir este post? Esta ação não pode ser desfeita.","posts.edit.delete_modal.action":"Excluir","posts.edit.delete_modal.cancel":"Cancelar","posts.edit.sync_enable.title":"Ativar sincronização?","posts.edit.sync_enable.description":"Todas as plataformas compartilharão o mesmo conteúdo. Qualquer edição personalizada feita em plataformas individuais será substituída pelo conteúdo atual.","posts.edit.sync_enable.cancel":"Cancelar","posts.edit.sync_enable.action":"Ativar sincronização","posts.edit.sync_disable.title":"Desativar sincronização?","posts.edit.sync_disable.description":"Cada plataforma manterá seu conteúdo atual, mas edições futuras serão aplicadas apenas à plataforma que você estiver editando.","posts.edit.sync_disable.customize_note":"Você poderá personalizar o conteúdo para cada plataforma individualmente.","posts.edit.sync_disable.cancel":"Cancelar","posts.edit.sync_disable.action":"Desativar sincronização","posts.edit.platforms_dialog.title":"Selecionar Plataformas","posts.edit.platforms_dialog.description":"Escolha em quais plataformas publicar este post.","posts.edit.hashtags_modal.search":"Buscar hashtags...","posts.edit.hashtags_modal.no_results":"Nenhuma hashtag encontrada.","posts.edit.validation.select_board":"Selecione uma pasta","posts.edit.validation.images_not_supported":"Imagens não suportadas","posts.edit.validation.videos_not_supported":"Vídeos não suportados","posts.edit.validation.max_images":"Máx :count imagens","posts.edit.validation.requires_media":"Requer mídia","posts.edit.validation.exceeded":":count excedido","posts.edit.validation.does_not_support_images":":platform não suporta imagens","posts.edit.validation.supports_up_to_images":":platform suporta até :count imagens","posts.edit.validation.does_not_support_videos":":platform não suporta vídeos","posts.content_types.instagram_feed.label":"Post do Feed","posts.content_types.instagram_feed.description":"Aparece no seu feed e perfil","posts.content_types.instagram_reel.label":"Reels","posts.content_types.instagram_reel.description":"Vídeo curto de até 90 segundos","posts.content_types.instagram_story.label":"Story","posts.content_types.instagram_story.description":"Desaparece após 24 horas","posts.content_types.linkedin_post.label":"Post","posts.content_types.linkedin_post.description":"Post padrão com texto e mídia","posts.content_types.linkedin_carousel.label":"Carrossel","posts.content_types.linkedin_carousel.description":"Imagens deslizáveis","posts.content_types.linkedin_page_post.label":"Post","posts.content_types.linkedin_page_post.description":"Post padrão com texto e mídia","posts.content_types.linkedin_page_carousel.label":"Carrossel","posts.content_types.linkedin_page_carousel.description":"Imagens deslizáveis","posts.content_types.facebook_post.label":"Post","posts.content_types.facebook_post.description":"Post padrão na sua página","posts.content_types.facebook_reel.label":"Reels","posts.content_types.facebook_reel.description":"Vídeo curto de até 90 segundos","posts.content_types.facebook_story.label":"Story","posts.content_types.facebook_story.description":"Desaparece após 24 horas","posts.content_types.tiktok_video.label":"Vídeo","posts.content_types.tiktok_video.description":"Conteúdo de vídeo curto","posts.content_types.youtube_short.label":"Short","posts.content_types.youtube_short.description":"Vídeo vertical de até 60 segundos","posts.content_types.x_post.label":"Post","posts.content_types.x_post.description":"Tweet com texto e mídia","posts.content_types.threads_post.label":"Post","posts.content_types.threads_post.description":"Post de texto com mídia opcional","posts.content_types.pinterest_pin.label":"Pin","posts.content_types.pinterest_pin.description":"Pin de imagem com link","posts.content_types.pinterest_video_pin.label":"Pin de Vídeo","posts.content_types.pinterest_video_pin.description":"Conteúdo em vídeo","posts.content_types.pinterest_carousel.label":"Carrossel","posts.content_types.pinterest_carousel.description":"2-5 imagens","posts.content_types.bluesky_post.label":"Post","posts.content_types.bluesky_post.description":"Post de texto com imagens opcionais","posts.content_types.mastodon_post.label":"Post","posts.content_types.mastodon_post.description":"Post de texto com mídia opcional","posts.platforms.linkedin":"LinkedIn","posts.platforms.linkedin-page":"Página do LinkedIn","posts.platforms.x":"X","posts.platforms.tiktok":"TikTok","posts.platforms.youtube":"YouTube Shorts","posts.platforms.facebook":"Página do Facebook","posts.platforms.instagram":"Instagram","posts.platforms.threads":"Threads","posts.platforms.pinterest":"Pinterest","posts.platforms.bluesky":"Bluesky","posts.platforms.mastodon":"Mastodon","posts.flash.scheduled":"Post agendado com sucesso!","posts.flash.publishing":"Post está sendo publicado!","posts.flash.deleted":"Post excluído com sucesso!","posts.flash.cannot_edit_published":"Posts publicados não podem ser editados.","posts.flash.connect_first":"Conecte pelo menos uma rede social antes de criar um post.","posts.errors.account_disconnected":"Conta social está desconectada","posts.errors.account_inactive":"Conta social está desativada","settings.title":"Configurações","settings.description":"Gerencie seu perfil e configurações da conta","settings.nav.profile":"Perfil","settings.nav.password":"Senha","settings.nav.workspace":"Workspace","settings.nav.members":"Membros","settings.nav.notifications":"Notificações","settings.nav.billing":"Faturamento","settings.notifications.title":"Preferências de notificações","settings.notifications.heading":"Notificações por e-mail","settings.notifications.description":"Escolha quais notificações por e-mail deseja receber","settings.notifications.post_published":"Post publicado","settings.notifications.post_published_description":"Receber um e-mail quando seu post for publicado com sucesso","settings.notifications.post_failed":"Post falhou","settings.notifications.post_failed_description":"Receber um e-mail quando seu post falhar ao publicar","settings.notifications.account_disconnected":"Conta desconectada","settings.notifications.account_disconnected_description":"Receber um e-mail quando uma conta social for desconectada","settings.notifications.save":"Salvar preferências","settings.profile.title":"Configurações do perfil","settings.profile.photo_heading":"Foto do perfil","settings.profile.photo_description":"Envie uma foto de perfil","settings.profile.heading":"Informações do perfil","settings.profile.description":"Atualize seu nome e endereço de e-mail","settings.profile.avatar":"Avatar","settings.profile.name":"Nome","settings.profile.name_placeholder":"Nome completo","settings.profile.email":"Endereço de e-mail","settings.profile.email_placeholder":"Endereço de e-mail","settings.profile.email_unverified":"Seu endereço de e-mail não foi verificado.","settings.profile.resend_verification":"Clique aqui para reenviar o e-mail de verificação.","settings.profile.verification_sent":"Um novo link de verificação foi enviado para seu endereço de e-mail.","settings.profile.save":"Salvar","settings.password.title":"Configurações de senha","settings.password.heading":"Atualizar senha","settings.password.description":"Certifique-se de que sua conta esteja usando uma senha longa e aleatória para se manter seguro","settings.password.current_password":"Senha atual","settings.password.current_password_placeholder":"Senha atual","settings.password.new_password":"Nova senha","settings.password.new_password_placeholder":"Nova senha","settings.password.confirm_password":"Confirmar senha","settings.password.confirm_password_placeholder":"Confirmar senha","settings.password.save":"Salvar senha","settings.delete_account.heading":"Excluir conta","settings.delete_account.description":"Exclua sua conta e todos os seus recursos","settings.delete_account.warning":"Atenção","settings.delete_account.warning_message":"Por favor, prossiga com cuidado, isso não pode ser desfeito.","settings.delete_account.button":"Excluir conta","settings.delete_account.modal_title":"Tem certeza que deseja excluir sua conta?","settings.delete_account.modal_description":"Uma vez que sua conta for excluída, todos os seus recursos e dados também serão permanentemente excluídos. Por favor, digite sua senha para confirmar que deseja excluir permanentemente sua conta.","settings.delete_account.password":"Senha","settings.delete_account.password_placeholder":"Senha","settings.delete_account.cancel":"Cancelar","settings.delete_account.confirm":"Excluir conta","settings.workspace.title":"Configurações do workspace","settings.workspace.logo_heading":"Logo do workspace","settings.workspace.logo_description":"Envie um logo para o workspace","settings.workspace.heading":"Nome do workspace","settings.workspace.description":"Atualize o nome e fuso horário do workspace","settings.workspace.members_heading":"Membros","settings.workspace.members_description":"Gerencie membros e convites do workspace","settings.workspace.name":"Nome","settings.workspace.name_placeholder":"Meu Workspace","settings.workspace.timezone":"Fuso horário","settings.workspace.save":"Salvar","settings.members.title":"Membros","settings.members.heading":"Membros da equipe","settings.members.description":"Gerencie membros e convites deste workspace","settings.members.cancel":"Cancelar","settings.members.remove":"Remover","settings.members.make_admin":"Tornar administrador","settings.members.make_member":"Tornar membro","settings.members.invite.title":"Convidar Membro","settings.members.invite.description":"Envie um convite por e-mail para adicionar colaboradores","settings.members.invite.email":"E-mail","settings.members.invite.email_placeholder":"colaborador@email.com","settings.members.invite.role":"Função","settings.members.invite.role_placeholder":"Selecione uma função","settings.members.invite.submit":"Enviar Convite","settings.members.pending.title":"Convites Pendentes","settings.members.pending.description":"Convites aguardando aceitação","settings.members.pending.empty":"Nenhum convite pendente","settings.members.list.title":"Membros","settings.members.list.description":"Pessoas com acesso a este workspace","settings.members.list.empty":"Nenhum membro além do proprietário","settings.members.remove_modal.title":"Remover membro","settings.members.remove_modal.description":"Tem certeza que deseja remover este membro do workspace? Ele perderá acesso a todos os recursos do workspace.","settings.members.remove_modal.action":"Remover membro","settings.members.cancel_invite_modal.title":"Cancelar convite","settings.members.cancel_invite_modal.description":"Tem certeza que deseja cancelar este convite?","settings.members.cancel_invite_modal.action":"Cancelar convite","settings.members.roles.owner":"Proprietário","settings.members.roles.admin":"Administrador","settings.members.roles.member":"Membro","settings.members.flash.invite_sent":"Convite enviado com sucesso!","settings.members.flash.invite_deleted":"Convite excluído.","settings.members.flash.member_removed":"Membro removido com sucesso.","settings.members.flash.role_updated":"Função do membro atualizada.","settings.members.flash.wrong_email":"Este convite é para um endereço de e-mail diferente.","settings.members.flash.already_member":"Você já é membro deste workspace.","settings.members.flash.invite_accepted":"Bem-vindo! Você agora é membro do workspace.","settings.members.flash.invite_declined":"Convite recusado.","settings.flash.profile_updated":"Perfil atualizado com sucesso!","settings.flash.language_updated":"Idioma atualizado com sucesso!","settings.flash.password_updated":"Senha atualizada com sucesso!","settings.flash.workspace_updated":"Configurações atualizadas com sucesso!","settings.flash.photo_updated":"Foto atualizada com sucesso!","settings.flash.photo_deleted":"Foto removida com sucesso!","settings.flash.logo_updated":"Logo enviado com sucesso!","settings.flash.logo_deleted":"Logo removido com sucesso!","settings.flash.notifications_updated":"Preferências de notificações atualizadas!","settings.api_keys.title":"Chaves API","settings.api_keys.page_title":"Chaves API","settings.api_keys.heading":"Chaves API","settings.api_keys.description":"Gerencie chaves API para acesso programático ao seu workspace.","settings.api_keys.create":"Criar chave API","settings.api_keys.copy":"Copiar","settings.api_keys.new_token_message":"Sua nova chave API foi criada. Copie agora — você não poderá vê-la novamente.","settings.api_keys.table.name":"Nome","settings.api_keys.table.key":"Chave","settings.api_keys.table.status":"Status","settings.api_keys.table.expires":"Expira","settings.api_keys.table.last_used":"Último uso","settings.api_keys.table.never":"Nunca","settings.api_keys.actions.copy_id":"Copiar ID da chave API","settings.api_keys.actions.copy_id_success":"ID da chave API copiado","settings.api_keys.actions.delete":"Excluir","settings.api_keys.empty.title":"Nenhuma chave API","settings.api_keys.empty.description":"Crie uma chave API para acessar seu workspace programaticamente.","settings.api_keys.delete_modal.title":"Excluir chave API","settings.api_keys.delete_modal.description":"Tem certeza que deseja excluir esta chave API? Aplicações que a usam perderão acesso imediatamente.","settings.api_keys.delete_modal.action":"Excluir chave API","settings.api_keys.create_dialog.title":"Criar chave API","settings.api_keys.create_dialog.description":"Crie uma nova chave API para acesso programático ao seu workspace.","settings.api_keys.create_dialog.name":"Nome","settings.api_keys.create_dialog.name_placeholder":"ex. Chave API de Produção","settings.api_keys.create_dialog.expires":"Data de expiração (opcional)","settings.api_keys.create_dialog.expires_placeholder":"Sem expiração","settings.api_keys.create_dialog.submit":"Criar","settings.api_keys.create_dialog.cancel":"Cancelar","settings.api_keys.flash.created":"Chave de API criada com sucesso!","settings.api_keys.flash.deleted":"Chave de API excluída com sucesso!","sidebar.workspaces":"Espaços de trabalho","sidebar.select_workspace":"Selecionar workspace","sidebar.create_workspace":"Criar workspace","sidebar.create_post":"Novo post","sidebar.profile":"Perfil","sidebar.account":"Conta","sidebar.log_out":"Sair","sidebar.workspace":"Workspace: :name","sidebar.workspace_select":"Workspace: Selecionar","sidebar.theme":"Tema: :name","sidebar.theme_light":"Claro","sidebar.theme_dark":"Escuro","sidebar.theme_system":"Sistema","sidebar.language":"Idioma: :name","sidebar.language_select":"Idioma: Selecionar","sidebar.groups.posts":"Posts","sidebar.groups.configuration":"Configurações","sidebar.groups.support":"Suporte","sidebar.posts.calendar":"Calendário","sidebar.posts.all":"Todos","sidebar.posts.scheduled":"Agendados","sidebar.posts.posted":"Publicados","sidebar.posts.drafts":"Rascunhos","sidebar.config.connections":"Conexões","sidebar.config.hashtags":"Hashtags","sidebar.config.labels":"Etiquetas","sidebar.config.settings":"Configurações","sidebar.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","validation.accepted":"O campo :attribute deve ser aceito.","validation.accepted_if":"O campo :attribute deve ser aceito quando :other for :value.","validation.active_url":"O campo :attribute deve ser uma URL válida.","validation.after":"O campo :attribute deve ser uma data posterior a :date.","validation.after_or_equal":"O campo :attribute deve ser uma data posterior ou igual a :date.","validation.alpha":"O campo :attribute deve conter apenas letras.","validation.alpha_dash":"O campo :attribute deve conter apenas letras, números, hifens e underscores.","validation.alpha_num":"O campo :attribute deve conter apenas letras e números.","validation.any_of":"O campo :attribute é inválido.","validation.array":"O campo :attribute deve ser um array.","validation.ascii":"O campo :attribute deve conter apenas caracteres alfanuméricos e símbolos de um byte.","validation.before":"O campo :attribute deve ser uma data anterior a :date.","validation.before_or_equal":"O campo :attribute deve ser uma data anterior ou igual a :date.","validation.between.array":"O campo :attribute deve ter entre :min e :max itens.","validation.between.file":"O campo :attribute deve estar entre :min e :max kilobytes.","validation.between.numeric":"O campo :attribute deve estar entre :min e :max.","validation.between.string":"O campo :attribute deve estar entre :min e :max caracteres.","validation.boolean":"O campo :attribute deve ser verdadeiro ou falso.","validation.can":"O campo :attribute contém um valor não autorizado.","validation.confirmed":"A confirmação do campo :attribute não corresponde.","validation.contains":"O campo :attribute está faltando um valor obrigatório.","validation.current_password":"A senha está incorreta.","validation.date":"O campo :attribute deve ser uma data válida.","validation.date_equals":"O campo :attribute deve ser uma data igual a :date.","validation.date_format":"O campo :attribute deve corresponder ao formato :format.","validation.decimal":"O campo :attribute deve ter :decimal casas decimais.","validation.declined":"O campo :attribute deve ser recusado.","validation.declined_if":"O campo :attribute deve ser recusado quando :other for :value.","validation.different":"O campo :attribute e :other devem ser diferentes.","validation.digits":"O campo :attribute deve ter :digits dígitos.","validation.digits_between":"O campo :attribute deve ter entre :min e :max dígitos.","validation.dimensions":"O campo :attribute deve ter dimensões de imagem válidas.","validation.distinct":"O campo :attribute tem um valor duplicado.","validation.doesnt_contain":"O campo :attribute não deve conter nenhum dos seguintes: :values.","validation.doesnt_end_with":"O campo :attribute não deve terminar com nenhum dos seguintes: :values.","validation.doesnt_start_with":"O campo :attribute não deve começar com nenhum dos seguintes: :values.","validation.email":"O campo :attribute deve ser um endereço de e-mail válido.","validation.encoding":"O campo :attribute deve ser codificado em :encoding.","validation.ends_with":"O campo :attribute deve terminar com um dos seguintes: :values.","validation.enum":"O :attribute selecionado é inválido.","validation.exists":"O :attribute selecionado é inválido.","validation.extensions":"O campo :attribute deve ter uma das seguintes extensões: :values.","validation.file":"O campo :attribute deve ser um arquivo.","validation.filled":"O campo :attribute deve ter um valor.","validation.gt.array":"O campo :attribute deve ter mais de :value itens.","validation.gt.file":"O campo :attribute deve ser maior que :value kilobytes.","validation.gt.numeric":"O campo :attribute deve ser maior que :value.","validation.gt.string":"O campo :attribute deve ser maior que :value caracteres.","validation.gte.array":"O campo :attribute deve ter :value itens ou mais.","validation.gte.file":"O campo :attribute deve ser maior ou igual a :value kilobytes.","validation.gte.numeric":"O campo :attribute deve ser maior ou igual a :value.","validation.gte.string":"O campo :attribute deve ser maior ou igual a :value caracteres.","validation.hex_color":"O campo :attribute deve ser uma cor hexadecimal válida.","validation.image":"O campo :attribute deve ser uma imagem.","validation.in":"O :attribute selecionado é inválido.","validation.in_array":"O campo :attribute deve existir em :other.","validation.in_array_keys":"O campo :attribute deve conter pelo menos uma das seguintes chaves: :values.","validation.integer":"O campo :attribute deve ser um inteiro.","validation.ip":"O campo :attribute deve ser um endereço IP válido.","validation.ipv4":"O campo :attribute deve ser um endereço IPv4 válido.","validation.ipv6":"O campo :attribute deve ser um endereço IPv6 válido.","validation.json":"O campo :attribute deve ser uma string JSON válida.","validation.list":"O campo :attribute deve ser uma lista.","validation.lowercase":"O campo :attribute deve estar em minúsculas.","validation.lt.array":"O campo :attribute deve ter menos de :value itens.","validation.lt.file":"O campo :attribute deve ser menor que :value kilobytes.","validation.lt.numeric":"O campo :attribute deve ser menor que :value.","validation.lt.string":"O campo :attribute deve ser menor que :value caracteres.","validation.lte.array":"O campo :attribute deve ter :value itens ou menos.","validation.lte.file":"O campo :attribute deve ser menor ou igual a :value kilobytes.","validation.lte.numeric":"O campo :attribute deve ser menor ou igual a :value.","validation.lte.string":"O campo :attribute deve ser menor ou igual a :value caracteres.","validation.mac_address":"O campo :attribute deve ser um endereço MAC válido.","validation.max.array":"O campo :attribute deve ter no máximo :max itens.","validation.max.file":"O campo :attribute deve ter no máximo :max kilobytes.","validation.max.numeric":"O campo :attribute deve ter no máximo :max.","validation.max.string":"O campo :attribute deve ter no máximo :max caracteres.","validation.max_digits":"O campo :attribute não deve ter mais que :max dígitos.","validation.mimes":"O campo :attribute deve ser um arquivo do tipo: :values.","validation.mimetypes":"O campo :attribute deve ser um arquivo do tipo: :values.","validation.min.array":"O campo :attribute deve ter pelo menos :min itens.","validation.min.file":"O campo :attribute deve ter pelo menos :min kilobytes.","validation.min.numeric":"O campo :attribute deve ter pelo menos :min.","validation.min.string":"O campo :attribute deve ter pelo menos :min caracteres.","validation.min_digits":"O campo :attribute deve ter pelo menos :min dígitos.","validation.missing":"O campo :attribute deve estar ausente.","validation.missing_if":"O campo :attribute deve estar ausente quando :other for :value.","validation.missing_unless":"O campo :attribute deve estar ausente a menos que :other seja :value.","validation.missing_with":"O campo :attribute deve estar ausente quando :values estiver presente.","validation.missing_with_all":"O campo :attribute deve estar ausente quando :values estiverem presentes.","validation.multiple_of":"O campo :attribute deve ser um múltiplo de :value.","validation.not_in":"O :attribute selecionado é inválido.","validation.not_regex":"O formato do campo :attribute é inválido.","validation.numeric":"O campo :attribute deve ser um número.","validation.password.letters":"O campo :attribute deve conter pelo menos uma letra.","validation.password.mixed":"O campo :attribute deve conter pelo menos uma letra maiúscula e uma minúscula.","validation.password.numbers":"O campo :attribute deve conter pelo menos um número.","validation.password.symbols":"O campo :attribute deve conter pelo menos um símbolo.","validation.password.uncompromised":"O :attribute fornecido apareceu em um vazamento de dados. Por favor, escolha um :attribute diferente.","validation.present":"O campo :attribute deve estar presente.","validation.present_if":"O campo :attribute deve estar presente quando :other for :value.","validation.present_unless":"O campo :attribute deve estar presente a menos que :other seja :value.","validation.present_with":"O campo :attribute deve estar presente quando :values estiver presente.","validation.present_with_all":"O campo :attribute deve estar presente quando :values estiverem presentes.","validation.prohibited":"O campo :attribute é proibido.","validation.prohibited_if":"O campo :attribute é proibido quando :other for :value.","validation.prohibited_if_accepted":"O campo :attribute é proibido quando :other for aceito.","validation.prohibited_if_declined":"O campo :attribute é proibido quando :other for recusado.","validation.prohibited_unless":"O campo :attribute é proibido a menos que :other esteja em :values.","validation.prohibits":"O campo :attribute proíbe :other de estar presente.","validation.regex":"O formato do campo :attribute é inválido.","validation.required":"O campo :attribute é obrigatório.","validation.required_array_keys":"O campo :attribute deve conter entradas para: :values.","validation.required_if":"O campo :attribute é obrigatório quando :other for :value.","validation.required_if_accepted":"O campo :attribute é obrigatório quando :other for aceito.","validation.required_if_declined":"O campo :attribute é obrigatório quando :other for recusado.","validation.required_unless":"O campo :attribute é obrigatório a menos que :other esteja em :values.","validation.required_with":"O campo :attribute é obrigatório quando :values estiver presente.","validation.required_with_all":"O campo :attribute é obrigatório quando :values estiverem presentes.","validation.required_without":"O campo :attribute é obrigatório quando :values não estiver presente.","validation.required_without_all":"O campo :attribute é obrigatório quando nenhum dos :values estiver presente.","validation.same":"O campo :attribute deve ser igual a :other.","validation.size.array":"O campo :attribute deve conter :size itens.","validation.size.file":"O campo :attribute deve ter :size kilobytes.","validation.size.numeric":"O campo :attribute deve ser :size.","validation.size.string":"O campo :attribute deve ter :size caracteres.","validation.starts_with":"O campo :attribute deve começar com um dos seguintes: :values.","validation.string":"O campo :attribute deve ser uma string.","validation.timezone":"O campo :attribute deve ser um fuso horário válido.","validation.unique":"O :attribute já foi utilizado.","validation.uploaded":"O :attribute falhou ao ser enviado.","validation.uppercase":"O campo :attribute deve estar em maiúsculo.","validation.url":"O campo :attribute deve ser uma URL válida.","validation.ulid":"O campo :attribute deve ser um ULID válido.","validation.uuid":"O campo :attribute deve ser um UUID válido.","validation.custom.attribute-name.rule-name":"custom-message","workspaces.title":"Workspaces","workspaces.select_title":"Seus workspaces","workspaces.select_description":"Selecione um workspace para continuar","workspaces.current":"Atual","workspaces.connections":":count conexões","workspaces.posts":":count posts","workspaces.create.title":"Criar um workspace","workspaces.create.description":"Dê um nome ao seu workspace para começar","workspaces.create.name":"Nome do workspace","workspaces.create.name_placeholder":"Meu Workspace","workspaces.create.submit":"Criar workspace"} \ No newline at end of file diff --git a/resources/js/components/AppSidebar.vue b/resources/js/components/AppSidebar.vue index 642ed98c..ab627421 100644 --- a/resources/js/components/AppSidebar.vue +++ b/resources/js/components/AppSidebar.vue @@ -197,7 +197,7 @@ const switchWorkspace = (workspaceId: string) => { - + @@ -206,7 +206,7 @@ const switchWorkspace = (workspaceId: string) => { - + {{ $t('sidebar.support.docs') }} diff --git a/resources/js/components/DatePicker.vue b/resources/js/components/DatePicker.vue index d4cd5495..529095c9 100644 --- a/resources/js/components/DatePicker.vue +++ b/resources/js/components/DatePicker.vue @@ -50,7 +50,7 @@ const props = defineProps({ const emit = defineEmits(['update:modelValue']); // Parse input value into date -function parseInput(value: string) { +const parseInput = (value: string) => { if (!value) return undefined; try { @@ -62,7 +62,7 @@ function parseInput(value: string) { return undefined; } return undefined; -} +}; const internalDate = ref(parseInput(props.modelValue)); const popoverOpen = ref(false); @@ -94,7 +94,7 @@ const minutes = computed(() => { }); // Build full datetime string -function buildDateTime(dateStr: string | null): string | null { +const buildDateTime = (dateStr: string | null): string | null => { if (!dateStr) return null; if (!props.showTime) { @@ -103,15 +103,14 @@ function buildDateTime(dateStr: string | null): string | null { const timeStr = `${selectedHour.value}:${selectedMinute.value}:00`; return `${dateStr}T${timeStr}`; -} +}; -// Handle time change -function onTimeChange() { +const onTimeChange = () => { if (internalDate.value) { const dateStr = internalDate.value.toString(); emit('update:modelValue', buildDateTime(dateStr)); } -} +}; // Parse input value into date component const isInternalUpdate = ref(false); diff --git a/resources/js/components/NotificationBell.vue b/resources/js/components/NotificationBell.vue index 54448dfc..f244bd4d 100644 --- a/resources/js/components/NotificationBell.vue +++ b/resources/js/components/NotificationBell.vue @@ -10,7 +10,9 @@ import { TooltipTrigger, } from '@/components/ui/tooltip'; import dayjs from '@/dayjs'; +import { accounts } from '@/routes/app'; import { index, read, readAll, archiveAll } from '@/routes/app/notifications'; +import { edit as editPost } from '@/routes/app/posts'; interface Notification { id: string; @@ -54,7 +56,7 @@ const handleMarkAsRead = async (notification: Notification) => { credentials: 'same-origin', }); - notification.read_at = new Date().toISOString(); + notification.read_at = dayjs().toISOString(); unreadCount.value = Math.max(0, unreadCount.value - 1); }; @@ -67,7 +69,7 @@ const handleMarkAllAsRead = async () => { notifications.value = notifications.value.map((n) => ({ ...n, - read_at: n.read_at ?? new Date().toISOString(), + read_at: n.read_at ?? dayjs().toISOString(), })); unreadCount.value = 0; }; @@ -91,9 +93,9 @@ const handleNotificationClick = (notification: Notification) => { close(); if (notification.data?.post_id) { - router.visit(`/posts/${notification.data.post_id}/edit`); + router.visit(editPost.url(notification.data.post_id)); } else if (notification.data?.social_account_id || notification.data?.workspace_id) { - router.visit('/accounts'); + router.visit(accounts.url()); } }; diff --git a/resources/js/components/WorkspaceSwitcher.vue b/resources/js/components/WorkspaceSwitcher.vue index 1600fbf8..15c14ca1 100644 --- a/resources/js/components/WorkspaceSwitcher.vue +++ b/resources/js/components/WorkspaceSwitcher.vue @@ -23,15 +23,15 @@ const page = usePage(); const currentWorkspace = computed(() => page.props.auth.currentWorkspace as Workspace | null); const workspaces = computed(() => page.props.auth.workspaces as Workspace[]); -function switchWorkspace(workspace: Workspace) { +const switchWorkspace = (workspace: Workspace) => { router.post(switchMethod.url(workspace.id), {}, { preserveScroll: true, }); -} +}; -function createWorkspace() { +const createWorkspace = () => { router.visit(createWorkspaceRoute.url()); -} +};