From 56b8c92e729f9a38c34a1260108d000ac37973d0 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Mon, 30 Mar 2026 00:20:43 -0300 Subject: [PATCH] refactor: settings redesign, Spanish translations, language system, strict_types Settings pages: - Redesign layout to match Sendkit (max-w-4xl, space-y-12, Separator sections) - Merge Members page into Workspace settings with Table, invite Dialog, ConfirmDeleteModal - Add workspace logo upload/delete routes and controller methods - Translate all hardcoded strings in Workspace.vue modals Language system: - Drop languages table, replace language_id FK with locale string column on users - Create config/languages.php for available languages and default locale - Add Spanish (es) translations (13 files) - Simplify HandleInertiaRequests, ProfileController, RegisteredUserController Code quality: - Add declare(strict_types=1) to all PHP files - Fix MastodonPublisher using wrong attribute (filename -> original_filename) - Fix HasMediaTest for new has_photo/photo_url accessors - Fix PublishToSocialPlatformTest type error revealed by strict_types - Remove orphaned Language model from AppServiceProvider morph map - Update User TypeScript interface (has_photo, photo_url, locale) - Eager load media relation on workspaces to prevent N+1 - Add 8 new tests for workspace logo upload/delete - Update workspace settings test to assert members/invitations props All 710 tests passing. --- app/Broadcasting/PostChannel.php | 2 + app/Concerns/PasswordValidationRules.php | 2 + app/Concerns/ProfileValidationRules.php | 2 + .../Commands/CheckSocialConnections.php | 2 + .../Commands/ProcessScheduledPosts.php | 2 + app/Enums/Media/Type.php | 2 + app/Enums/Post/Status.php | 2 + app/Enums/PostPlatform/ContentType.php | 2 + app/Enums/SocialAccount/Platform.php | 2 + app/Enums/SocialAccount/Status.php | 2 + app/Enums/User/Persona.php | 2 + app/Enums/User/Setup.php | 2 + app/Enums/UserWorkspace/Role.php | 2 + app/Events/PostPlatformStatusUpdated.php | 2 + app/Events/SubscriptionCreated.php | 2 + app/Exceptions/TokenExpiredException.php | 2 + app/Helpers/upload.php | 2 + .../App/Settings/ProfileController.php | 6 +- .../Controllers/App/WorkspaceController.php | 17 + .../Controllers/Auth/BlueskyController.php | 2 + .../Controllers/Auth/FacebookController.php | 2 + .../Controllers/Auth/InstagramController.php | 2 + .../Controllers/Auth/LinkedInController.php | 2 + .../Auth/LinkedInPageController.php | 2 + .../Controllers/Auth/MastodonController.php | 2 + .../Controllers/Auth/PinterestController.php | 2 + .../Auth/RegisteredUserController.php | 5 +- .../Controllers/Auth/SocialController.php | 2 + .../Controllers/Auth/ThreadsController.php | 2 + .../Controllers/Auth/TikTokController.php | 2 + app/Http/Controllers/Auth/XController.php | 2 + .../Controllers/Auth/YouTubeController.php | 2 + app/Http/Controllers/Controller.php | 2 + app/Http/Middleware/App/EnsureSubscribed.php | 2 + .../App/EnsureUserSetupIsComplete.php | 2 + app/Http/Middleware/App/HandleAppearance.php | 2 + .../Middleware/App/HandleInertiaRequests.php | 15 +- .../Invite/StoreWorkspaceInviteRequest.php | 2 + .../App/Media/StoreChunkedMediaRequest.php | 2 + .../Requests/App/Media/StoreMediaRequest.php | 2 + .../Requests/App/Post/UpdatePostRequest.php | 2 + .../App/Settings/PasswordUpdateRequest.php | 2 + .../App/Settings/ProfileDeleteRequest.php | 2 + .../App/Settings/ProfileUpdateRequest.php | 2 + .../App/Workspace/StoreWorkspaceRequest.php | 2 + .../App/Workspace/UpdateWorkspaceRequest.php | 2 + .../AuthUserResource.php | 2 +- app/Jobs/PublishPost.php | 2 + app/Jobs/PublishToSocialPlatform.php | 2 + app/Jobs/VerifyWorkspaceConnections.php | 2 + app/Mail/AccountDisconnected.php | 2 + app/Mail/WorkspaceConnectionsDisconnected.php | 4 +- app/Mail/WorkspaceInvite.php | 2 + app/Models/Language.php | 23 -- app/Models/Media.php | 2 + app/Models/Post.php | 2 + app/Models/PostPlatform.php | 2 + app/Models/SocialAccount.php | 2 + app/Models/Traits/HasMedia.php | 2 + app/Models/Traits/HasWorkspace.php | 2 + app/Models/User.php | 11 +- app/Models/Workspace.php | 2 + app/Models/WorkspaceHashtag.php | 2 + app/Models/WorkspaceLabel.php | 2 + app/Policies/WorkspacePolicy.php | 2 + app/Providers/AppServiceProvider.php | 4 +- app/Providers/HorizonServiceProvider.php | 2 + app/Providers/TelescopeServiceProvider.php | 2 + app/Services/Social/BlueskyPublisher.php | 2 + app/Services/Social/ConnectionVerifier.php | 2 + app/Services/Social/FacebookPublisher.php | 2 + app/Services/Social/InstagramPublisher.php | 2 + app/Services/Social/LinkedInPagePublisher.php | 2 + app/Services/Social/LinkedInPublisher.php | 2 + .../Social/LinkedInTokenSynchronizer.php | 2 + app/Services/Social/MastodonPublisher.php | 4 +- app/Services/Social/PinterestPublisher.php | 2 + app/Services/Social/ThreadsPublisher.php | 2 + app/Services/Social/TikTokPublisher.php | 2 + app/Services/Social/XPublisher.php | 2 + app/Services/Social/YouTubePublisher.php | 2 + app/Socialite/InstagramProvider.php | 2 + app/Socialite/LinkedInPageExtendSocialite.php | 2 + config/ai.php | 2 + config/app.php | 2 + config/auth.php | 2 + config/broadcasting.php | 2 + config/cache.php | 2 + config/cashier.php | 2 + config/chunk-upload.php | 2 + config/database.php | 2 + config/filesystems.php | 2 + config/horizon.php | 2 + config/inertia.php | 2 + config/languages.php | 35 ++ config/logging.php | 2 + config/mail.php | 2 + config/queue.php | 2 + config/reverb.php | 2 + config/services.php | 2 + config/session.php | 2 + config/telescope.php | 2 + config/trypost.php | 2 + database/factories/ApiTokenFactory.php | 2 + database/factories/LanguageFactory.php | 25 -- database/factories/MediaFactory.php | 2 + database/factories/PostFactory.php | 2 + database/factories/PostPlatformFactory.php | 2 + database/factories/SocialAccountFactory.php | 2 + database/factories/UserFactory.php | 5 +- database/factories/WorkspaceFactory.php | 2 + .../factories/WorkspaceHashtagFactory.php | 2 + database/factories/WorkspaceInviteFactory.php | 2 + database/factories/WorkspaceLabelFactory.php | 2 + .../0001_01_01_000000_create_users_table.php | 2 + .../0001_01_01_000001_create_cache_table.php | 2 + .../0001_01_01_000002_create_jobs_table.php | 2 + ...1_14_231040_create_subscriptions_table.php | 2 + ...231041_create_subscription_items_table.php | 2 + ...6_01_14_232315_create_workspaces_table.php | 2 + ..._14_232316_create_user_workspace_table.php | 2 + ...14_232317_create_social_accounts_table.php | 2 + .../2026_01_14_232318_create_posts_table.php | 2 + ..._14_232319_create_post_platforms_table.php | 2 + .../2026_01_14_232320_create_medias_table.php | 2 + ..._233835_create_workspace_invites_table.php | 2 + ...152319_create_workspace_hashtags_table.php | 2 + ...7_153347_create_workspace_labels_table.php | 2 + ...3538_create_post_workspace_label_table.php | 2 + ...32520_create_agent_conversations_table.php | 2 + ..._233024_create_telescope_entries_table.php | 2 + ...place_language_id_with_locale_on_users.php | 53 +++ database/seeders/DatabaseSeeder.php | 6 +- database/seeders/LanguageSeeder.php | 18 -- lang/en/settings.php | 31 +- lang/es/accounts.php | 68 ++++ lang/es/auth.php | 82 +++++ lang/es/billing.php | 37 +++ lang/es/calendar.php | 12 + lang/es/hashtags.php | 48 +++ lang/es/labels.php | 42 +++ lang/es/mail.php | 15 + lang/es/pagination.php | 6 + lang/es/passwords.php | 9 + lang/es/posts.php | 227 +++++++++++++ lang/es/settings.php | 142 ++++++++ lang/es/sidebar.php | 49 +++ lang/es/validation.php | 165 ++++++++++ lang/php_en.json | 1 - lang/php_pt-br.json | 1 - lang/pt-br/settings.php | 31 +- resources/js/components/LanguageCombobox.vue | 97 ------ resources/js/components/UserMenuContent.vue | 27 +- resources/js/components/ui/table/Table.vue | 16 + .../js/components/ui/table/TableBody.vue | 17 + .../js/components/ui/table/TableCaption.vue | 17 + .../js/components/ui/table/TableCell.vue | 22 ++ .../js/components/ui/table/TableFooter.vue | 17 + .../js/components/ui/table/TableHead.vue | 17 + .../js/components/ui/table/TableHeader.vue | 17 + resources/js/components/ui/table/TableRow.vue | 17 + resources/js/components/ui/table/index.ts | 8 + resources/js/layouts/settings/Layout.vue | 40 +-- resources/js/pages/settings/Profile.vue | 149 ++++----- resources/js/pages/settings/Workspace.vue | 303 +++++++++++++++--- resources/js/types/index.d.ts | 6 +- routes/app.php | 2 +- routes/channels.php | 2 + routes/console.php | 2 + routes/web.php | 2 + tests/Feature/AcceptInviteControllerTest.php | 4 +- tests/Feature/Api/ApiKeyApiTest.php | 2 + .../Feature/Api/AuthenticateApiTokenTest.php | 2 + tests/Feature/Api/HashtagApiTest.php | 2 + tests/Feature/Api/LabelApiTest.php | 2 + tests/Feature/Api/WorkspaceApiTest.php | 2 + tests/Feature/Auth/AuthenticationTest.php | 2 + tests/Feature/Auth/EmailVerificationTest.php | 2 + tests/Feature/Auth/PasswordResetTest.php | 2 + tests/Feature/Auth/RegistrationTest.php | 2 + .../Auth/VerificationNotificationTest.php | 2 + tests/Feature/BillingControllerTest.php | 2 + .../Commands/ProcessScheduledPostsTest.php | 2 + .../Feature/Controllers/ChunkedUploadTest.php | 2 + tests/Feature/ExampleTest.php | 2 + tests/Feature/Jobs/PublishPostTest.php | 2 + .../Jobs/PublishToSocialPlatformTest.php | 4 +- .../Listeners/StripeEventListenerTest.php | 2 + tests/Feature/MediaControllerTest.php | 2 + .../Middleware/EnsureSubscribedTest.php | 2 + .../EnsureUserSetupIsCompleteTest.php | 2 + tests/Feature/OnboardingControllerTest.php | 2 + tests/Feature/PostControllerTest.php | 2 + .../Social/ConnectionVerifierTest.php | 2 + tests/Feature/Settings/PasswordUpdateTest.php | 2 + tests/Feature/Settings/ProfileUpdateTest.php | 16 +- .../Feature/Social/BlueskyControllerTest.php | 2 + .../Feature/Social/FacebookControllerTest.php | 2 + .../Social/InstagramControllerTest.php | 2 + .../Feature/Social/LinkedInControllerTest.php | 2 + .../Social/LinkedInPageControllerTest.php | 2 + .../Feature/Social/MastodonControllerTest.php | 2 + .../Social/PinterestControllerTest.php | 2 + .../Feature/Social/ThreadsControllerTest.php | 2 + tests/Feature/Social/TikTokControllerTest.php | 2 + tests/Feature/Social/XControllerTest.php | 2 + .../Feature/Social/YouTubeControllerTest.php | 2 + tests/Feature/SocialControllerTest.php | 2 + .../VerifyWorkspaceConnectionsTest.php | 2 + tests/Feature/WorkspaceControllerTest.php | 86 ++++- .../WorkspaceHashtagControllerTest.php | 2 + .../Feature/WorkspaceInviteControllerTest.php | 27 +- .../Feature/WorkspaceLabelControllerTest.php | 2 + tests/Pest.php | 2 + tests/Unit/Broadcasting/PostChannelTest.php | 2 + tests/Unit/Enums/ContentTypeTest.php | 2 + tests/Unit/Enums/MediaTypeTest.php | 2 + tests/Unit/Enums/PlatformTest.php | 2 + tests/Unit/Enums/PostStatusTest.php | 2 + tests/Unit/Enums/RoleTest.php | 2 + tests/Unit/Enums/SocialAccountStatusTest.php | 2 + tests/Unit/Enums/UserSetupTest.php | 2 + .../Events/PostPlatformStatusUpdatedTest.php | 2 + tests/Unit/Events/SubscriptionCreatedTest.php | 2 + tests/Unit/ExampleTest.php | 2 + tests/Unit/HelpersTest.php | 2 + .../Listeners/StripeEventListenerTest.php | 2 + tests/Unit/Mail/AccountDisconnectedTest.php | 2 + tests/Unit/Mail/WorkspaceInviteTest.php | 2 + tests/Unit/Models/LanguageTest.php | 22 -- tests/Unit/Models/MediaTest.php | 2 + tests/Unit/Models/PostTest.php | 2 + tests/Unit/Models/WorkspaceHashtagTest.php | 2 + tests/Unit/Models/WorkspaceLabelTest.php | 2 + tests/Unit/Policies/WorkspacePolicyTest.php | 2 + tests/Unit/Rules/TimezoneRuleTest.php | 2 + .../Unit/Services/InstagramPublisherTest.php | 2 + tests/Unit/Services/LinkedInPublisherTest.php | 2 + .../Services/Social/BlueskyPublisherTest.php | 2 + .../Services/Social/FacebookPublisherTest.php | 2 + .../Social/InstagramPublisherTest.php | 2 + .../Social/LinkedInPagePublisherTest.php | 2 + .../Services/Social/LinkedInPublisherTest.php | 2 + .../Social/LinkedInTokenSynchronizerTest.php | 2 + .../Services/Social/MastodonPublisherTest.php | 2 + .../Social/PinterestPublisherTest.php | 2 + .../Services/Social/ThreadsPublisherTest.php | 2 + .../Services/Social/TikTokPublisherTest.php | 2 + tests/Unit/Services/Social/XPublisherTest.php | 2 + .../Services/Social/YouTubePublisherTest.php | 2 + tests/Unit/Services/XPublisherTest.php | 2 + tests/Unit/Services/YouTubePublisherTest.php | 2 + .../Unit/Socialite/InstagramProviderTest.php | 2 + tests/Unit/Traits/HasMediaTest.php | 18 +- tests/Unit/Traits/HasWorkspaceTest.php | 2 + 255 files changed, 2117 insertions(+), 429 deletions(-) delete mode 100644 app/Models/Language.php create mode 100644 config/languages.php delete mode 100644 database/factories/LanguageFactory.php create mode 100644 database/migrations/2026_03_30_024008_replace_language_id_with_locale_on_users.php delete mode 100644 database/seeders/LanguageSeeder.php create mode 100644 lang/es/accounts.php create mode 100644 lang/es/auth.php create mode 100644 lang/es/billing.php create mode 100644 lang/es/calendar.php create mode 100644 lang/es/hashtags.php create mode 100644 lang/es/labels.php create mode 100644 lang/es/mail.php create mode 100644 lang/es/pagination.php create mode 100644 lang/es/passwords.php create mode 100644 lang/es/posts.php create mode 100644 lang/es/settings.php create mode 100644 lang/es/sidebar.php create mode 100644 lang/es/validation.php delete mode 100644 lang/php_en.json delete mode 100644 lang/php_pt-br.json delete mode 100644 resources/js/components/LanguageCombobox.vue create mode 100644 resources/js/components/ui/table/Table.vue create mode 100644 resources/js/components/ui/table/TableBody.vue create mode 100644 resources/js/components/ui/table/TableCaption.vue create mode 100644 resources/js/components/ui/table/TableCell.vue create mode 100644 resources/js/components/ui/table/TableFooter.vue create mode 100644 resources/js/components/ui/table/TableHead.vue create mode 100644 resources/js/components/ui/table/TableHeader.vue create mode 100644 resources/js/components/ui/table/TableRow.vue create mode 100644 resources/js/components/ui/table/index.ts delete mode 100644 tests/Unit/Models/LanguageTest.php diff --git a/app/Broadcasting/PostChannel.php b/app/Broadcasting/PostChannel.php index de8639b6..b653acc2 100644 --- a/app/Broadcasting/PostChannel.php +++ b/app/Broadcasting/PostChannel.php @@ -1,5 +1,7 @@ validate([ - 'language_id' => ['required', 'exists:languages,id'], + 'locale' => ['required', 'string', 'in:'.implode(',', array_keys(config('languages.available')))], ]); $request->user()->update([ - 'language_id' => $request->language_id, + 'locale' => $request->locale, ]); - $request->user()->refresh(); - session()->flash('flash.banner', __('settings.flash.language_updated')); session()->flash('flash.bannerStyle', 'success'); diff --git a/app/Http/Controllers/App/WorkspaceController.php b/app/Http/Controllers/App/WorkspaceController.php index 4f2b11ba..72f645e7 100644 --- a/app/Http/Controllers/App/WorkspaceController.php +++ b/app/Http/Controllers/App/WorkspaceController.php @@ -86,8 +86,25 @@ public function settings(Request $request): Response|RedirectResponse ->mapWithKeys(fn ($tz) => [$tz => $tz]) ->toArray(); + $members = $workspace->members() + ->get() + ->map(fn ($member) => [ + 'id' => $member->id, + 'name' => $member->name, + 'email' => $member->email, + 'role' => $member->pivot->role, + 'is_owner' => $member->id === $workspace->user_id, + ]); + + $invitations = $workspace->invites() + ->select('id', 'email', 'role') + ->latest() + ->get(); + return Inertia::render('settings/Workspace', [ 'workspace' => $workspace, + 'members' => $members, + 'invitations' => $invitations, 'timezones' => $timezones, ]); } diff --git a/app/Http/Controllers/Auth/BlueskyController.php b/app/Http/Controllers/Auth/BlueskyController.php index 1ed869b7..31326248 100644 --- a/app/Http/Controllers/Auth/BlueskyController.php +++ b/app/Http/Controllers/Auth/BlueskyController.php @@ -1,5 +1,7 @@ input('redirect', ''), '/invites/'); $user = DB::transaction(function () use ($request, $isInviteRegistration) { - $defaultLanguage = Language::where('code', 'en')->first(); - $user = User::create([ 'name' => $request->name, 'email' => $request->email, 'password' => $request->password, 'setup' => $isInviteRegistration ? Setup::Completed : Setup::Role, - 'language_id' => $defaultLanguage?->id, + 'locale' => config('languages.default'), 'email_verified_at' => $isInviteRegistration ? now() : null, ]); diff --git a/app/Http/Controllers/Auth/SocialController.php b/app/Http/Controllers/Auth/SocialController.php index 42dbf9dd..5ed0e076 100644 --- a/app/Http/Controllers/Auth/SocialController.php +++ b/app/Http/Controllers/Auth/SocialController.php @@ -1,5 +1,7 @@ user(); - if ($user?->language) { - App::setLocale($user->language->code); + if ($user?->locale) { + App::setLocale($user->locale); } - $currentWorkspace = $user?->currentWorkspace; + $currentWorkspace = $user?->currentWorkspace?->load('media'); return [ ...parent::share($request), @@ -41,7 +39,7 @@ public function share(Request $request): array 'user' => $user ? AuthUserResource::make($user) : null, 'currentWorkspace' => $currentWorkspace ? AuthWorkspaceResource::make($currentWorkspace, $user) : null, 'workspaces' => $user - ? $user->workspaces()->get()->map(fn ($ws) => AuthWorkspaceResource::summary($ws)) + ? $user->workspaces()->with('media')->get()->map(fn ($ws) => AuthWorkspaceResource::summary($ws)) : [], ], 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', @@ -49,7 +47,10 @@ public function share(Request $request): array 'applicationUrl' => config('app.url'), 'env' => config('app.env'), 'locale' => app()->getLocale(), - 'languages' => fn () => Cache::remember('languages:public', 3600, fn () => Language::query()->orderBy('name')->get()->toArray()), + 'languages' => collect(config('languages.available'))->map(fn ($name, $code) => [ + 'code' => $code, + 'name' => $name, + ])->values()->all(), 'selfHosted' => config('trypost.self_hosted'), ]; } diff --git a/app/Http/Requests/App/Invite/StoreWorkspaceInviteRequest.php b/app/Http/Requests/App/Invite/StoreWorkspaceInviteRequest.php index 5ec53661..fc1e701c 100644 --- a/app/Http/Requests/App/Invite/StoreWorkspaceInviteRequest.php +++ b/app/Http/Requests/App/Invite/StoreWorkspaceInviteRequest.php @@ -1,5 +1,7 @@ $user->photo_url, 'email_verified_at' => $user->email_verified_at?->toIso8601String(), 'current_workspace_id' => $user->current_workspace_id, - 'language_id' => $user->language_id, + 'locale' => $user->locale, 'created_at' => $user->created_at->toIso8601String(), 'updated_at' => $user->updated_at->toIso8601String(), ]; diff --git a/app/Jobs/PublishPost.php b/app/Jobs/PublishPost.php index 8bb4e6c4..a9e5a320 100644 --- a/app/Jobs/PublishPost.php +++ b/app/Jobs/PublishPost.php @@ -1,5 +1,7 @@ locale = $this->workspace->owner?->language?->code ?? 'en'; + $this->locale = $this->workspace->owner?->locale ?? 'en'; } public function envelope(): Envelope diff --git a/app/Mail/WorkspaceInvite.php b/app/Mail/WorkspaceInvite.php index 47df0c7f..e4da2e59 100644 --- a/app/Mail/WorkspaceInvite.php +++ b/app/Mail/WorkspaceInvite.php @@ -1,5 +1,7 @@ hasMany(User::class); - } -} diff --git a/app/Models/Media.php b/app/Models/Media.php index 33031c0a..e9f17f7c 100644 --- a/app/Models/Media.php +++ b/app/Models/Media.php @@ -1,5 +1,7 @@ belongsTo(Language::class); + return $this->locale ?? 'en'; } /** diff --git a/app/Models/Workspace.php b/app/Models/Workspace.php index 27193668..0d942d9f 100644 --- a/app/Models/Workspace.php +++ b/app/Models/Workspace.php @@ -1,5 +1,7 @@ Language::class, 'media' => Media::class, 'post' => Post::class, 'postPlatform' => PostPlatform::class, diff --git a/app/Providers/HorizonServiceProvider.php b/app/Providers/HorizonServiceProvider.php index c7d739b7..a0361957 100644 --- a/app/Providers/HorizonServiceProvider.php +++ b/app/Providers/HorizonServiceProvider.php @@ -1,5 +1,7 @@ take(4) as $media) { - $mediaId = $this->uploadMedia($account, $instance, $media->url, $media->filename); + $mediaId = $this->uploadMedia($account, $instance, $media->url, $media->original_filename); if ($mediaId) { $mediaIds[] = $mediaId; } diff --git a/app/Services/Social/PinterestPublisher.php b/app/Services/Social/PinterestPublisher.php index df345a85..f15d42ca 100644 --- a/app/Services/Social/PinterestPublisher.php +++ b/app/Services/Social/PinterestPublisher.php @@ -1,5 +1,7 @@ [ + 'en' => 'English', + 'es' => 'Español', + 'pt-br' => 'Português', + ], + + /* + |-------------------------------------------------------------------------- + | Default Language + |-------------------------------------------------------------------------- + | + | The default locale code assigned to new users. + | + */ + + 'default' => 'en', + +]; diff --git a/config/logging.php b/config/logging.php index 9e998a49..aa0d9feb 100644 --- a/config/logging.php +++ b/config/logging.php @@ -1,5 +1,7 @@ - */ -class LanguageFactory extends Factory -{ - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array - { - return [ - 'name' => 'English', - 'code' => 'en', - ]; - } -} diff --git a/database/factories/MediaFactory.php b/database/factories/MediaFactory.php index 2760ce40..5c62ba5f 100644 --- a/database/factories/MediaFactory.php +++ b/database/factories/MediaFactory.php @@ -1,5 +1,7 @@ static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), 'current_workspace_id' => null, - 'language_id' => Language::factory(), + 'locale' => 'en', 'stripe_id' => null, 'pm_type' => null, 'pm_last_four' => null, diff --git a/database/factories/WorkspaceFactory.php b/database/factories/WorkspaceFactory.php index b7b285de..c698e81e 100644 --- a/database/factories/WorkspaceFactory.php +++ b/database/factories/WorkspaceFactory.php @@ -1,5 +1,7 @@ string('locale', 10)->default('en')->after('current_workspace_id'); + }); + + // Migrate existing language_id to locale code + DB::statement(' + UPDATE users + SET locale = COALESCE( + (SELECT code FROM languages WHERE languages.id = users.language_id), + \'en\' + ) + '); + + Schema::table('users', function (Blueprint $table) { + $table->dropForeign(['language_id']); + $table->dropColumn('language_id'); + }); + + Schema::dropIfExists('languages'); + } + + public function down(): void + { + Schema::create('languages', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->string('name'); + $table->string('code'); + $table->timestamps(); + }); + + Schema::table('users', function (Blueprint $table) { + $table->uuid('language_id')->nullable()->after('current_workspace_id'); + $table->foreign('language_id')->references('id')->on('languages')->nullOnDelete(); + }); + + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('locale'); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 563e208b..a19a78b1 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -1,5 +1,7 @@ call([ - LanguageSeeder::class, - ]); + // } } diff --git a/database/seeders/LanguageSeeder.php b/database/seeders/LanguageSeeder.php deleted file mode 100644 index d1902ad2..00000000 --- a/database/seeders/LanguageSeeder.php +++ /dev/null @@ -1,18 +0,0 @@ - 'English', 'code' => 'en']); - Language::create(['name' => 'Português', 'code' => 'pt-br']); - } -} diff --git a/lang/en/settings.php b/lang/en/settings.php index ba8becf3..76973cee 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -14,6 +14,8 @@ 'profile' => [ 'title' => 'Profile settings', + 'photo_heading' => 'Profile photo', + 'photo_description' => 'Upload a profile photo', 'heading' => 'Profile information', 'description' => 'Update your name and email address', 'avatar' => 'Avatar', @@ -56,14 +58,16 @@ 'workspace' => [ 'title' => 'Workspace settings', - 'heading' => 'Workspace settings', - 'description' => 'Update your workspace name, logo, and timezone', - 'logo' => 'Logo', + 'logo_heading' => 'Workspace logo', + 'logo_description' => 'Upload a logo for your workspace', + 'heading' => 'Workspace name', + 'description' => 'Update your workspace name and timezone', + 'members_heading' => 'Members', + 'members_description' => 'Manage workspace members and invitations', 'name' => 'Name', 'name_placeholder' => 'My Workspace', 'timezone' => 'Timezone', 'save' => 'Save', - 'saved' => 'Saved.', ], 'members' => [ @@ -71,6 +75,9 @@ 'heading' => 'Team members', 'description' => 'Manage members and invites for this workspace', + 'cancel' => 'Cancel', + 'remove' => 'Remove', + 'invite' => [ 'title' => 'Invite Member', 'description' => 'Send an email invite to add collaborators', @@ -79,7 +86,6 @@ 'role' => 'Role', 'role_placeholder' => 'Select a role', 'submit' => 'Send Invite', - 'cancel_confirm' => 'Are you sure you want to cancel this invite?', ], 'pending' => [ @@ -92,7 +98,18 @@ 'title' => 'Members', 'description' => 'People with access to this workspace', 'empty' => 'No members besides the owner', - 'remove_confirm' => 'Are you sure you want to remove this member?', + ], + + 'remove_modal' => [ + 'title' => 'Remove member', + 'description' => 'Are you sure you want to remove this member from the workspace? They will lose access to all workspace resources.', + 'action' => 'Remove member', + ], + + 'cancel_invite_modal' => [ + 'title' => 'Cancel invitation', + 'description' => 'Are you sure you want to cancel this invitation?', + 'action' => 'Cancel invitation', ], 'roles' => [ @@ -119,5 +136,7 @@ 'workspace_updated' => 'Settings updated successfully!', 'photo_updated' => 'Photo updated successfully!', 'photo_deleted' => 'Photo removed successfully!', + 'logo_updated' => 'Logo uploaded successfully!', + 'logo_deleted' => 'Logo removed successfully!', ], ]; diff --git a/lang/es/accounts.php b/lang/es/accounts.php new file mode 100644 index 00000000..33b64d18 --- /dev/null +++ b/lang/es/accounts.php @@ -0,0 +1,68 @@ + 'Conexiones', + 'page_title' => 'Cuentas conectadas', + 'description' => 'Conecta tus redes sociales para programar y publicar posts', + + 'not_connected' => 'No conectado', + 'connect' => 'Conectar', + 'connection_lost' => 'Conexión perdida', + 'reconnect_account' => 'Reconectar cuenta', + 'view_profile' => 'Ver perfil', + 'disconnect' => 'Desconectar', + + 'disconnect_modal' => [ + 'title' => 'Desconectar cuenta', + 'description' => '¿Estás seguro de que deseas desconectar esta cuenta? Puedes volver a conectarla en cualquier momento.', + 'confirm' => 'Desconectar', + 'cancel' => 'Cancelar', + ], + + 'bluesky' => [ + 'title' => 'Conectar Bluesky', + 'description' => 'Introduce tus credenciales para conectar', + 'email' => 'Correo electrónico', + 'email_placeholder' => 'tuusuario.bsky.social', + 'app_password' => 'Contraseña de app', + 'app_password_placeholder' => 'xxxx-xxxx-xxxx-xxxx', + 'app_password_hint' => 'Usa una Contraseña de App por seguridad. Crea una en bsky.app/settings.', + 'submit' => 'Conectar Bluesky', + 'submitting' => 'Conectando...', + ], + + 'mastodon' => [ + 'title' => 'Conectar Mastodon', + 'description' => 'Introduce tu instancia de Mastodon', + 'instance_url' => 'URL de la instancia', + 'instance_placeholder' => 'https://mastodon.social', + 'instance_hint' => 'Introduce la URL de tu instancia de Mastodon (ej: mastodon.social, techhub.social)', + 'submit' => 'Continuar con Mastodon', + 'submitting' => 'Conectando...', + ], + + 'facebook' => [ + 'title' => 'Seleccionar página de Facebook', + 'description' => 'Elige qué página deseas conectar', + 'no_pages' => 'No se encontraron páginas', + 'no_pages_description' => 'No eres administrador de ninguna página de Facebook.', + 'page_label' => 'Página de Facebook', + ], + + 'linkedin' => [ + 'title' => 'Seleccionar página de LinkedIn', + 'description' => 'Elige qué página deseas conectar', + 'no_pages' => 'No se encontraron páginas', + 'no_pages_description' => 'No eres administrador de ninguna página de LinkedIn.', + 'page_label' => 'Página de LinkedIn', + ], + + 'flash' => [ + 'disconnected' => '¡Cuenta desconectada correctamente!', + 'connected' => '¡Cuenta conectada correctamente!', + 'session_expired' => 'Sesión expirada. Inténtalo de nuevo.', + 'workspace_not_found' => 'Workspace no encontrado.', + 'already_connected' => 'Esta plataforma ya está conectada.', + 'no_youtube_channels' => 'No se encontraron canales de YouTube. Crea un canal primero.', + ], +]; diff --git a/lang/es/auth.php b/lang/es/auth.php new file mode 100644 index 00000000..ba016ab9 --- /dev/null +++ b/lang/es/auth.php @@ -0,0 +1,82 @@ + 'Estas credenciales no coinciden con nuestros registros.', + 'password' => 'La contraseña proporcionada es incorrecta.', + 'throttle' => 'Demasiados intentos de inicio de sesión. Inténtalo de nuevo en :seconds segundos.', + + 'flash' => [ + 'welcome' => '¡Bienvenido a TryPost!', + 'welcome_trial' => '¡Bienvenido a TryPost! Tu prueba ha comenzado.', + ], + + 'login' => [ + 'title' => 'Inicia sesión en tu cuenta', + 'description' => 'Introduce tu correo y contraseña para iniciar sesión', + 'page_title' => 'Iniciar sesión', + 'email' => 'Correo electrónico', + 'password' => 'Contraseña', + 'forgot_password' => '¿Olvidaste tu contraseña?', + 'remember_me' => 'Recuérdame', + 'submit' => 'Iniciar sesión', + 'no_account' => '¿No tienes una cuenta?', + 'sign_up' => 'Regístrate', + ], + + 'register' => [ + 'title' => 'Crear una cuenta', + 'description' => 'Introduce tus datos para crear tu cuenta', + 'page_title' => 'Registro', + 'name' => 'Nombre', + 'name_placeholder' => 'Nombre completo', + 'email' => 'Correo electrónico', + 'password' => 'Contraseña', + 'submit' => 'Crear cuenta', + 'has_account' => '¿Ya tienes una cuenta?', + 'log_in' => 'Iniciar sesión', + ], + + 'forgot_password' => [ + 'title' => 'Olvidé mi contraseña', + 'description' => 'Introduce tu correo para recibir un enlace de restablecimiento', + 'page_title' => 'Olvidé mi contraseña', + 'email' => 'Correo electrónico', + 'submit' => 'Enviar enlace de restablecimiento', + 'return_to' => 'O vuelve a', + 'log_in' => 'iniciar sesión', + ], + + 'reset_password' => [ + 'title' => 'Restablecer contraseña', + 'description' => 'Introduce tu nueva contraseña', + 'page_title' => 'Restablecer contraseña', + 'email' => 'Correo electrónico', + 'password' => 'Contraseña', + 'confirm_password' => 'Confirmar contraseña', + 'confirm_placeholder' => 'Confirmar contraseña', + 'submit' => 'Restablecer contraseña', + ], + + 'verify_email' => [ + 'title' => 'Verificar correo', + 'description' => 'Verifica tu correo electrónico haciendo clic en el enlace que acabamos de enviarte.', + 'page_title' => 'Verificación de correo', + 'link_sent' => 'Se ha enviado un nuevo enlace de verificación al correo electrónico proporcionado durante el registro.', + 'resend' => 'Reenviar correo de verificación', + 'log_out' => 'Cerrar sesión', + ], + + 'accept_invite' => [ + 'page_title' => 'Aceptar invitación', + 'title' => '¡Has sido invitado!', + 'description' => 'Has sido invitado a unirte al workspace :workspace.', + 'workspace' => 'Workspace', + 'your_role' => 'Tu rol', + 'email' => 'Correo electrónico', + 'accept' => 'Aceptar invitación', + 'decline' => 'Rechazar invitación', + 'login_prompt' => 'Inicia sesión o crea una cuenta para aceptar esta invitación.', + 'log_in' => 'Iniciar sesión', + 'create_account' => 'Crear cuenta', + ], +]; diff --git a/lang/es/billing.php b/lang/es/billing.php new file mode 100644 index 00000000..0d54e732 --- /dev/null +++ b/lang/es/billing.php @@ -0,0 +1,37 @@ + 'Suscripción', + 'description' => 'Administra tu suscripción y método de pago', + + 'trial' => [ + 'title' => 'Periodo de prueba activo', + 'description' => 'Tu prueba termina el :date. Después, tu suscripción se cobrará automáticamente.', + ], + + 'subscription' => [ + 'title' => 'Tu suscripción', + 'workspaces' => 'Workspaces', + 'quantity' => 'Cantidad de suscripción', + 'expires' => 'Expira :date', + 'canceled_on' => 'Tu suscripción se cancelará el :date', + 'manage' => 'Administrar en Stripe', + ], + + 'invoices' => [ + 'title' => 'Facturas', + 'description' => 'Historial de pagos', + 'empty' => 'No se encontraron facturas', + 'paid' => 'Pagado', + ], + + 'status' => [ + 'active' => 'Activa', + 'canceled' => 'Cancelada', + 'incomplete' => 'Incompleta', + 'incomplete_expired' => 'Expirada', + 'past_due' => 'Vencida', + 'trialing' => 'Prueba', + 'unpaid' => 'Sin pagar', + ], +]; diff --git a/lang/es/calendar.php b/lang/es/calendar.php new file mode 100644 index 00000000..7d0e5d8e --- /dev/null +++ b/lang/es/calendar.php @@ -0,0 +1,12 @@ + 'Calendario', + 'today' => 'Hoy', + 'day' => 'Día', + 'week' => 'Semana', + 'month' => 'Mes', + 'new_post' => 'Nuevo post', + 'no_content' => 'Sin contenido', + 'more' => '+:count más', +]; diff --git a/lang/es/hashtags.php b/lang/es/hashtags.php new file mode 100644 index 00000000..116c355d --- /dev/null +++ b/lang/es/hashtags.php @@ -0,0 +1,48 @@ + 'Hashtags', + 'description' => 'Crea grupos de hashtags para agregarlos rápidamente a tus posts', + 'new_group' => 'Nuevo grupo', + 'no_groups_yet' => 'Aún no hay grupos de hashtags', + 'no_groups_description' => 'Crea grupos de hashtags para agregar rápidamente hashtags populares a tus posts', + 'create_first_group' => 'Crea tu primer grupo', + 'hashtags_count' => ':count hashtags', + + 'create' => [ + 'title' => 'Crear grupo de hashtags', + 'description' => 'Dale un nombre a tu grupo y agrega hashtags separados por espacios o comas', + 'name' => 'Nombre del grupo', + 'name_placeholder' => 'ej: Marketing, Viajes, Comida', + 'hashtags' => 'Hashtags', + 'hashtags_placeholder' => '#marketing #redessociales #negocios #crecimiento', + 'hashtags_hint' => 'Ingresa hashtags separados por espacios o comas. Incluye el símbolo #.', + 'submit' => 'Crear grupo', + 'submitting' => 'Creando...', + ], + + 'edit' => [ + 'title' => 'Editar grupo de hashtags', + 'description' => 'Actualiza el nombre y los hashtags de este grupo', + 'name' => 'Nombre del grupo', + 'name_placeholder' => 'ej: Marketing, Viajes, Comida', + 'hashtags' => 'Hashtags', + 'hashtags_placeholder' => '#marketing #redessociales #negocios #crecimiento', + 'hashtags_hint' => 'Ingresa hashtags separados por espacios o comas. Incluye el símbolo #.', + 'submit' => 'Guardar cambios', + 'submitting' => 'Guardando...', + ], + + 'delete' => [ + 'title' => 'Eliminar grupo de hashtags', + 'description' => '¿Estás seguro de que deseas eliminar este grupo de hashtags? Esta acción no se puede deshacer.', + 'confirm' => 'Eliminar', + 'cancel' => 'Cancelar', + ], + + 'flash' => [ + 'created' => '¡Grupo de hashtags creado correctamente!', + 'updated' => '¡Grupo de hashtags actualizado correctamente!', + 'deleted' => '¡Grupo de hashtags eliminado correctamente!', + ], +]; diff --git a/lang/es/labels.php b/lang/es/labels.php new file mode 100644 index 00000000..8800e959 --- /dev/null +++ b/lang/es/labels.php @@ -0,0 +1,42 @@ + 'Etiquetas', + 'description' => 'Crea etiquetas para organizar y categorizar tus posts', + 'new_label' => 'Nueva etiqueta', + 'no_labels_yet' => 'Aún no hay etiquetas', + 'create_first_label' => 'Crea tu primera etiqueta', + + 'create' => [ + 'title' => 'Crear etiqueta', + 'description' => 'Dale un nombre y elige un color para tu etiqueta', + 'name' => 'Nombre', + 'name_placeholder' => 'Nombre de la etiqueta...', + 'color' => 'Color', + 'submit' => 'Crear etiqueta', + 'submitting' => 'Creando...', + ], + + 'edit' => [ + 'title' => 'Editar etiqueta', + 'description' => 'Actualiza el nombre y el color de esta etiqueta', + 'name' => 'Nombre', + 'name_placeholder' => 'Nombre de la etiqueta...', + 'color' => 'Color', + 'submit' => 'Guardar cambios', + 'submitting' => 'Guardando...', + ], + + 'delete' => [ + 'title' => 'Eliminar etiqueta', + 'description' => '¿Estás seguro de que deseas eliminar esta etiqueta? Esta acción no se puede deshacer.', + 'confirm' => 'Eliminar', + 'cancel' => 'Cancelar', + ], + + 'flash' => [ + 'created' => '¡Etiqueta creada correctamente!', + 'updated' => '¡Etiqueta actualizada correctamente!', + 'deleted' => '¡Etiqueta eliminada correctamente!', + ], +]; diff --git a/lang/es/mail.php b/lang/es/mail.php new file mode 100644 index 00000000..c99ce05a --- /dev/null +++ b/lang/es/mail.php @@ -0,0 +1,15 @@ + [ + 'subject' => '{1} :count cuenta necesita ser reconectada en :workspace|[2,*] :count cuentas necesitan ser reconectadas en :workspace', + 'title' => 'Cuentas necesitan reconexión', + 'intro' => 'Las siguientes cuentas sociales en tu workspace :workspace se han desconectado y necesitan ser reconectadas:', + 'reasons_title' => 'Esto puede haber ocurrido porque:', + 'reason_expired' => 'Los tokens de acceso expiraron', + 'reason_revoked' => 'Revocaste el acceso a TryPost en la plataforma', + 'reason_changed' => 'La plataforma cambió sus requisitos de autenticación', + 'reconnect_cta' => 'Reconecta estas cuentas para seguir programando y publicando posts.', + 'button' => 'Reconectar cuentas', + ], +]; diff --git a/lang/es/pagination.php b/lang/es/pagination.php new file mode 100644 index 00000000..1bed3bd0 --- /dev/null +++ b/lang/es/pagination.php @@ -0,0 +1,6 @@ + '« Anterior', + 'next' => 'Siguiente »', +]; diff --git a/lang/es/passwords.php b/lang/es/passwords.php new file mode 100644 index 00000000..66292cb3 --- /dev/null +++ b/lang/es/passwords.php @@ -0,0 +1,9 @@ + 'Tu contraseña ha sido restablecida.', + 'sent' => 'Te hemos enviado un enlace para restablecer tu contraseña.', + 'throttled' => 'Espera antes de intentarlo de nuevo.', + 'token' => 'Este token de restablecimiento de contraseña no es válido.', + 'user' => 'No encontramos un usuario con ese correo electrónico.', +]; diff --git a/lang/es/posts.php b/lang/es/posts.php new file mode 100644 index 00000000..e9d261f0 --- /dev/null +++ b/lang/es/posts.php @@ -0,0 +1,227 @@ + 'Posts', + 'all_posts' => 'Todos los posts', + 'new_post' => 'Nuevo post', + 'no_posts' => 'No se encontraron posts', + 'start_creating' => 'Empieza creando tu primer post.', + 'manage_posts' => 'Administra todos tus posts', + 'delete_confirm' => '¿Estás seguro de que deseas eliminar este post?', + 'by' => 'por', + + 'actions' => [ + 'view' => 'Ver post', + 'delete' => 'Eliminar post', + ], + + 'form' => [ + 'post_type' => 'Tipo de post', + 'board' => 'Tablero', + 'select_board' => 'Seleccionar tablero', + 'search_board' => 'Buscar tablero...', + 'no_board_found' => 'No se encontró tablero', + 'media' => 'Multimedia', + 'min' => 'Min', + 'uploading' => 'Subiendo...', + 'drop_to_upload' => 'Suelta para subir', + 'drag_and_drop' => 'Arrastra y suelta o haz clic para subir', + 'photos_and_videos' => 'Fotos y videos', + 'photos_only' => 'Solo fotos', + 'videos_only' => 'Solo videos', + 'drag_to_reorder' => 'Arrastra para reordenar', + 'caption' => 'Descripción', + 'write_caption' => 'Escribe tu descripción...', + ], + + 'status' => [ + 'draft' => 'Borrador', + 'scheduled' => 'Programado', + 'publishing' => 'Publicando', + 'published' => 'Publicado', + 'partially_published' => 'Parcialmente publicado', + 'failed' => 'Fallido', + ], + + 'descriptions' => [ + 'draft' => 'Posts esperando ser programados', + 'scheduled' => 'Posts programados para publicar', + 'published' => 'Posts ya publicados', + ], + + 'edit' => [ + 'title' => 'Editar post', + 'view_title' => 'Ver post', + 'manage_platforms' => 'Administrar plataformas', + 'sync' => 'Sincronizar', + 'labels' => 'Etiquetas', + 'hashtags' => 'Hashtags', + 'schedule' => 'Programar', + 'publish' => 'Publicar', + 'delete' => 'Eliminar', + 'settings' => 'Configuración', + 'schedule_for' => 'Programar para', + 'saving' => 'Guardando...', + 'saved' => 'Guardado', + 'scheduled_at' => 'Programado:', + 'published_at' => 'Publicado:', + 'media' => 'Multimedia', + 'caption' => 'Descripción', + 'no_caption' => 'Sin descripción', + 'no_content' => 'Sin contenido', + + 'empty_state' => [ + 'title' => 'No hay plataformas seleccionadas', + 'description' => 'Selecciona al menos una plataforma para crear tu post', + ], + + 'delete_modal' => [ + 'title' => 'Eliminar post', + 'description' => '¿Estás seguro de que deseas eliminar este post? Esta acción no se puede deshacer.', + 'action' => 'Eliminar', + 'cancel' => 'Cancelar', + ], + + 'sync_enable' => [ + 'title' => '¿Activar sincronización?', + 'description' => 'Todas las plataformas compartirán el mismo contenido. Las ediciones personalizadas realizadas en plataformas individuales serán reemplazadas con el contenido actual.', + 'cancel' => 'Cancelar', + 'action' => 'Activar sincronización', + ], + + 'sync_disable' => [ + 'title' => '¿Desactivar sincronización?', + 'description' => 'Cada plataforma mantendrá su contenido actual, pero las ediciones futuras solo se aplicarán a la plataforma que estés editando.', + 'customize_note' => 'Podrás personalizar el contenido de cada plataforma individualmente.', + 'cancel' => 'Cancelar', + 'action' => 'Desactivar sincronización', + ], + + 'platforms_dialog' => [ + 'title' => 'Seleccionar plataformas', + 'description' => 'Elige en qué plataformas publicar este post.', + ], + + 'hashtags_modal' => [ + 'search' => 'Buscar hashtags...', + 'no_results' => 'No se encontraron hashtags.', + ], + + 'validation' => [ + 'select_board' => 'Selecciona un tablero', + 'images_not_supported' => 'Imágenes no soportadas', + 'videos_not_supported' => 'Videos no soportados', + 'max_images' => 'Máximo :count imágenes', + 'requires_media' => 'Requiere multimedia', + 'exceeded' => ':count excedido', + 'does_not_support_images' => ':platform no soporta imágenes', + 'supports_up_to_images' => ':platform soporta hasta :count imágenes', + 'does_not_support_videos' => ':platform no soporta videos', + ], + ], + + 'content_types' => [ + 'instagram_feed' => [ + 'label' => 'Post del feed', + 'description' => 'Aparece en tu feed y perfil', + ], + 'instagram_reel' => [ + 'label' => 'Reel', + 'description' => 'Video corto de hasta 90 segundos', + ], + 'instagram_story' => [ + 'label' => 'Historia', + 'description' => 'Desaparece después de 24 horas', + ], + 'linkedin_post' => [ + 'label' => 'Post', + 'description' => 'Post estándar con texto y multimedia', + ], + 'linkedin_carousel' => [ + 'label' => 'Carrusel', + 'description' => 'Imágenes deslizables', + ], + 'linkedin_page_post' => [ + 'label' => 'Post', + 'description' => 'Post estándar con texto y multimedia', + ], + 'linkedin_page_carousel' => [ + 'label' => 'Carrusel', + 'description' => 'Imágenes deslizables', + ], + 'facebook_post' => [ + 'label' => 'Post', + 'description' => 'Post estándar en tu página', + ], + 'facebook_reel' => [ + 'label' => 'Reel', + 'description' => 'Video corto de hasta 90 segundos', + ], + 'facebook_story' => [ + 'label' => 'Historia', + 'description' => 'Desaparece después de 24 horas', + ], + 'tiktok_video' => [ + 'label' => 'Video', + 'description' => 'Contenido de video corto', + ], + 'youtube_short' => [ + 'label' => 'Short', + 'description' => 'Video vertical de hasta 60 segundos', + ], + 'x_post' => [ + 'label' => 'Post', + 'description' => 'Tweet con texto y multimedia', + ], + 'threads_post' => [ + 'label' => 'Post', + 'description' => 'Post de texto con multimedia opcional', + ], + 'pinterest_pin' => [ + 'label' => 'Pin', + 'description' => 'Pin de imagen con enlace', + ], + 'pinterest_video_pin' => [ + 'label' => 'Pin de video', + 'description' => 'Contenido de video', + ], + 'pinterest_carousel' => [ + 'label' => 'Carrusel', + 'description' => '2-5 imágenes', + ], + 'bluesky_post' => [ + 'label' => 'Post', + 'description' => 'Post de texto con imágenes opcionales', + ], + 'mastodon_post' => [ + 'label' => 'Post', + 'description' => 'Post de texto con multimedia opcional', + ], + ], + + 'platforms' => [ + 'linkedin' => 'LinkedIn', + 'linkedin-page' => 'Página de LinkedIn', + 'x' => 'X', + 'tiktok' => 'TikTok', + 'youtube' => 'YouTube Shorts', + 'facebook' => 'Página de Facebook', + 'instagram' => 'Instagram', + 'threads' => 'Threads', + 'pinterest' => 'Pinterest', + 'bluesky' => 'Bluesky', + 'mastodon' => 'Mastodon', + ], + + 'flash' => [ + 'scheduled' => '¡Post programado correctamente!', + 'publishing' => '¡El post se está publicando!', + 'deleted' => '¡Post eliminado correctamente!', + 'cannot_edit_published' => 'Los posts publicados no se pueden editar.', + 'connect_first' => 'Conecta al menos una red social antes de crear un post.', + ], + + 'errors' => [ + 'account_disconnected' => 'Cuenta social desconectada', + ], +]; diff --git a/lang/es/settings.php b/lang/es/settings.php new file mode 100644 index 00000000..24eb7a73 --- /dev/null +++ b/lang/es/settings.php @@ -0,0 +1,142 @@ + 'Configuración', + 'description' => 'Administra tu perfil y configuración de la cuenta', + + 'nav' => [ + 'profile' => 'Perfil', + 'password' => 'Contraseña', + 'workspace' => 'Workspace', + 'members' => 'Miembros', + 'billing' => 'Facturación', + ], + + 'profile' => [ + 'title' => 'Configuración del perfil', + 'photo_heading' => 'Foto de perfil', + 'photo_description' => 'Sube una foto de perfil', + 'heading' => 'Información del perfil', + 'description' => 'Actualiza tu nombre y correo electrónico', + 'avatar' => 'Avatar', + 'name' => 'Nombre', + 'name_placeholder' => 'Nombre completo', + 'email' => 'Correo electrónico', + 'email_placeholder' => 'Correo electrónico', + 'email_unverified' => 'Tu correo electrónico no ha sido verificado.', + 'resend_verification' => 'Haz clic aquí para reenviar el correo de verificación.', + 'verification_sent' => 'Se ha enviado un nuevo enlace de verificación a tu correo electrónico.', + 'save' => 'Guardar', + ], + + 'password' => [ + 'title' => 'Configuración de contraseña', + 'heading' => 'Actualizar contraseña', + 'description' => 'Asegúrate de que tu cuenta use una contraseña larga y aleatoria para mantenerte seguro', + 'current_password' => 'Contraseña actual', + 'current_password_placeholder' => 'Contraseña actual', + 'new_password' => 'Nueva contraseña', + 'new_password_placeholder' => 'Nueva contraseña', + 'confirm_password' => 'Confirmar contraseña', + 'confirm_password_placeholder' => 'Confirmar contraseña', + 'save' => 'Guardar contraseña', + ], + + 'delete_account' => [ + 'heading' => 'Eliminar cuenta', + 'description' => 'Elimina tu cuenta y todos sus recursos', + 'warning' => 'Advertencia', + 'warning_message' => 'Procede con precaución, esta acción no se puede deshacer.', + 'button' => 'Eliminar cuenta', + 'modal_title' => '¿Estás seguro de que deseas eliminar tu cuenta?', + '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.', + 'password' => 'Contraseña', + 'password_placeholder' => 'Contraseña', + 'cancel' => 'Cancelar', + 'confirm' => 'Eliminar cuenta', + ], + + 'workspace' => [ + 'title' => 'Configuración del workspace', + 'logo_heading' => 'Logo del workspace', + 'logo_description' => 'Sube un logo para tu workspace', + 'heading' => 'Nombre del workspace', + 'description' => 'Actualiza el nombre y zona horaria del workspace', + 'members_heading' => 'Miembros', + 'members_description' => 'Administra miembros e invitaciones del workspace', + 'name' => 'Nombre', + 'name_placeholder' => 'Mi Workspace', + 'timezone' => 'Zona horaria', + 'save' => 'Guardar', + ], + + 'members' => [ + 'title' => 'Miembros', + 'heading' => 'Miembros del equipo', + 'description' => 'Administra miembros e invitaciones de este workspace', + + 'cancel' => 'Cancelar', + 'remove' => 'Eliminar', + + 'invite' => [ + 'title' => 'Invitar miembro', + 'description' => 'Envía una invitación por correo para agregar colaboradores', + 'email' => 'Correo electrónico', + 'email_placeholder' => 'colaborador@email.com', + 'role' => 'Rol', + 'role_placeholder' => 'Selecciona un rol', + 'submit' => 'Enviar invitación', + ], + + 'pending' => [ + 'title' => 'Invitaciones pendientes', + 'description' => 'Invitaciones en espera de aceptación', + 'empty' => 'No hay invitaciones pendientes', + ], + + 'list' => [ + 'title' => 'Miembros', + 'description' => 'Personas con acceso a este workspace', + 'empty' => 'No hay miembros además del propietario', + ], + + 'remove_modal' => [ + 'title' => 'Eliminar miembro', + 'description' => '¿Estás seguro de que deseas eliminar a este miembro del workspace? Perderá acceso a todos los recursos del workspace.', + 'action' => 'Eliminar miembro', + ], + + 'cancel_invite_modal' => [ + 'title' => 'Cancelar invitación', + 'description' => '¿Estás seguro de que deseas cancelar esta invitación?', + 'action' => 'Cancelar invitación', + ], + + 'roles' => [ + 'owner' => 'Propietario', + 'admin' => 'Administrador', + 'member' => 'Miembro', + ], + + 'flash' => [ + 'invite_sent' => '¡Invitación enviada correctamente!', + 'invite_deleted' => 'Invitación eliminada.', + 'member_removed' => '¡Miembro eliminado correctamente!', + 'wrong_email' => 'Esta invitación es para otro correo electrónico.', + 'already_member' => 'Ya eres miembro de este workspace.', + 'invite_accepted' => '¡Bienvenido! Ahora eres miembro del workspace.', + 'invite_declined' => 'Invitación rechazada.', + ], + ], + + 'flash' => [ + 'profile_updated' => '¡Perfil actualizado correctamente!', + 'language_updated' => '¡Idioma actualizado correctamente!', + 'password_updated' => '¡Contraseña actualizada correctamente!', + 'workspace_updated' => '¡Configuración actualizada correctamente!', + 'photo_updated' => '¡Foto actualizada correctamente!', + 'photo_deleted' => '¡Foto eliminada correctamente!', + 'logo_updated' => '¡Logo subido correctamente!', + 'logo_deleted' => '¡Logo eliminado correctamente!', + ], +]; diff --git a/lang/es/sidebar.php b/lang/es/sidebar.php new file mode 100644 index 00000000..0dacf4e3 --- /dev/null +++ b/lang/es/sidebar.php @@ -0,0 +1,49 @@ + 'Workspaces', + 'select_workspace' => 'Seleccionar workspace', + 'create_workspace' => 'Crear workspace', + 'create_post' => 'Crear post', + 'profile' => 'Perfil', + 'log_out' => 'Cerrar sesión', + + 'workspace' => 'Workspace: :name', + 'workspace_select' => 'Workspace: Seleccionar', + + 'theme' => 'Tema: :name', + 'theme_light' => 'Claro', + 'theme_dark' => 'Oscuro', + 'theme_system' => 'Sistema', + + 'language' => 'Idioma: :name', + 'language_select' => 'Idioma: Seleccionar', + + 'groups' => [ + 'posts' => 'Posts', + 'configuration' => 'Configuración', + 'support' => 'Soporte', + ], + + 'posts' => [ + 'calendar' => 'Calendario', + 'all' => 'Todos', + 'scheduled' => 'Programados', + 'posted' => 'Publicados', + 'drafts' => 'Borradores', + ], + + 'config' => [ + 'connections' => 'Conexiones', + 'hashtags' => 'Hashtags', + 'labels' => 'Etiquetas', + 'settings' => 'Configuración', + ], + + 'support' => [ + 'discord' => 'Discord', + 'share_feedback' => 'Dar feedback', + 'last_updates' => 'Últimas actualizaciones', + 'docs' => 'Documentación', + ], +]; diff --git a/lang/es/validation.php b/lang/es/validation.php new file mode 100644 index 00000000..bb7a9e00 --- /dev/null +++ b/lang/es/validation.php @@ -0,0 +1,165 @@ + 'El campo :attribute debe ser aceptado.', + 'accepted_if' => 'El campo :attribute debe ser aceptado cuando :other es :value.', + 'active_url' => 'El campo :attribute debe ser una URL válida.', + 'after' => 'El campo :attribute debe ser una fecha posterior a :date.', + 'after_or_equal' => 'El campo :attribute debe ser una fecha posterior o igual a :date.', + 'alpha' => 'El campo :attribute solo puede contener letras.', + 'alpha_dash' => 'El campo :attribute solo puede contener letras, números, guiones y guiones bajos.', + 'alpha_num' => 'El campo :attribute solo puede contener letras y números.', + 'any_of' => 'El campo :attribute no es válido.', + 'array' => 'El campo :attribute debe ser un arreglo.', + 'ascii' => 'El campo :attribute solo puede contener caracteres alfanuméricos de un byte y símbolos.', + 'before' => 'El campo :attribute debe ser una fecha anterior a :date.', + 'before_or_equal' => 'El campo :attribute debe ser una fecha anterior o igual a :date.', + 'between' => [ + 'array' => 'El campo :attribute debe tener entre :min y :max elementos.', + 'file' => 'El campo :attribute debe pesar entre :min y :max kilobytes.', + 'numeric' => 'El campo :attribute debe estar entre :min y :max.', + 'string' => 'El campo :attribute debe tener entre :min y :max caracteres.', + ], + 'boolean' => 'El campo :attribute debe ser verdadero o falso.', + 'can' => 'El campo :attribute contiene un valor no autorizado.', + 'confirmed' => 'La confirmación del campo :attribute no coincide.', + 'contains' => 'Al campo :attribute le falta un valor requerido.', + 'current_password' => 'La contraseña es incorrecta.', + 'date' => 'El campo :attribute debe ser una fecha válida.', + 'date_equals' => 'El campo :attribute debe ser una fecha igual a :date.', + 'date_format' => 'El campo :attribute debe coincidir con el formato :format.', + 'decimal' => 'El campo :attribute debe tener :decimal decimales.', + 'declined' => 'El campo :attribute debe ser rechazado.', + 'declined_if' => 'El campo :attribute debe ser rechazado cuando :other es :value.', + 'different' => 'El campo :attribute y :other deben ser diferentes.', + 'digits' => 'El campo :attribute debe tener :digits dígitos.', + 'digits_between' => 'El campo :attribute debe tener entre :min y :max dígitos.', + 'dimensions' => 'El campo :attribute tiene dimensiones de imagen no válidas.', + 'distinct' => 'El campo :attribute tiene un valor duplicado.', + 'doesnt_contain' => 'El campo :attribute no debe contener ninguno de los siguientes: :values.', + 'doesnt_end_with' => 'El campo :attribute no debe terminar con uno de los siguientes: :values.', + 'doesnt_start_with' => 'El campo :attribute no debe comenzar con uno de los siguientes: :values.', + 'email' => 'El campo :attribute debe ser un correo electrónico válido.', + 'encoding' => 'El campo :attribute debe estar codificado en :encoding.', + 'ends_with' => 'El campo :attribute debe terminar con uno de los siguientes: :values.', + 'enum' => 'El :attribute seleccionado no es válido.', + 'exists' => 'El :attribute seleccionado no es válido.', + 'extensions' => 'El campo :attribute debe tener una de las siguientes extensiones: :values.', + 'file' => 'El campo :attribute debe ser un archivo.', + 'filled' => 'El campo :attribute debe tener un valor.', + 'gt' => [ + 'array' => 'El campo :attribute debe tener más de :value elementos.', + 'file' => 'El campo :attribute debe pesar más de :value kilobytes.', + 'numeric' => 'El campo :attribute debe ser mayor que :value.', + 'string' => 'El campo :attribute debe tener más de :value caracteres.', + ], + 'gte' => [ + 'array' => 'El campo :attribute debe tener :value elementos o más.', + 'file' => 'El campo :attribute debe pesar :value kilobytes o más.', + 'numeric' => 'El campo :attribute debe ser mayor o igual a :value.', + 'string' => 'El campo :attribute debe tener :value caracteres o más.', + ], + 'hex_color' => 'El campo :attribute debe ser un color hexadecimal válido.', + 'image' => 'El campo :attribute debe ser una imagen.', + 'in' => 'El :attribute seleccionado no es válido.', + 'in_array' => 'El campo :attribute debe existir en :other.', + 'in_array_keys' => 'El campo :attribute debe contener al menos una de las siguientes claves: :values.', + 'integer' => 'El campo :attribute debe ser un número entero.', + 'ip' => 'El campo :attribute debe ser una dirección IP válida.', + 'ipv4' => 'El campo :attribute debe ser una dirección IPv4 válida.', + 'ipv6' => 'El campo :attribute debe ser una dirección IPv6 válida.', + 'json' => 'El campo :attribute debe ser una cadena JSON válida.', + 'list' => 'El campo :attribute debe ser una lista.', + 'lowercase' => 'El campo :attribute debe estar en minúsculas.', + 'lt' => [ + 'array' => 'El campo :attribute debe tener menos de :value elementos.', + 'file' => 'El campo :attribute debe pesar menos de :value kilobytes.', + 'numeric' => 'El campo :attribute debe ser menor que :value.', + 'string' => 'El campo :attribute debe tener menos de :value caracteres.', + ], + 'lte' => [ + 'array' => 'El campo :attribute no debe tener más de :value elementos.', + 'file' => 'El campo :attribute debe pesar :value kilobytes o menos.', + 'numeric' => 'El campo :attribute debe ser menor o igual a :value.', + 'string' => 'El campo :attribute debe tener :value caracteres o menos.', + ], + 'mac_address' => 'El campo :attribute debe ser una dirección MAC válida.', + 'max' => [ + 'array' => 'El campo :attribute no debe tener más de :max elementos.', + 'file' => 'El campo :attribute no debe pesar más de :max kilobytes.', + 'numeric' => 'El campo :attribute no debe ser mayor que :max.', + 'string' => 'El campo :attribute no debe tener más de :max caracteres.', + ], + 'max_digits' => 'El campo :attribute no debe tener más de :max dígitos.', + 'mimes' => 'El campo :attribute debe ser un archivo de tipo: :values.', + 'mimetypes' => 'El campo :attribute debe ser un archivo de tipo: :values.', + 'min' => [ + 'array' => 'El campo :attribute debe tener al menos :min elementos.', + 'file' => 'El campo :attribute debe pesar al menos :min kilobytes.', + 'numeric' => 'El campo :attribute debe ser al menos :min.', + 'string' => 'El campo :attribute debe tener al menos :min caracteres.', + ], + 'min_digits' => 'El campo :attribute debe tener al menos :min dígitos.', + 'missing' => 'El campo :attribute debe estar ausente.', + 'missing_if' => 'El campo :attribute debe estar ausente cuando :other es :value.', + 'missing_unless' => 'El campo :attribute debe estar ausente a menos que :other sea :value.', + 'missing_with' => 'El campo :attribute debe estar ausente cuando :values está presente.', + 'missing_with_all' => 'El campo :attribute debe estar ausente cuando :values están presentes.', + 'multiple_of' => 'El campo :attribute debe ser múltiplo de :value.', + 'not_in' => 'El :attribute seleccionado no es válido.', + 'not_regex' => 'El formato del campo :attribute no es válido.', + 'numeric' => 'El campo :attribute debe ser un número.', + 'password' => [ + 'letters' => 'El campo :attribute debe contener al menos una letra.', + 'mixed' => 'El campo :attribute debe contener al menos una letra mayúscula y una minúscula.', + 'numbers' => 'El campo :attribute debe contener al menos un número.', + 'symbols' => 'El campo :attribute debe contener al menos un símbolo.', + 'uncompromised' => 'El :attribute proporcionado ha aparecido en una filtración de datos. Elige un :attribute diferente.', + ], + 'present' => 'El campo :attribute debe estar presente.', + 'present_if' => 'El campo :attribute debe estar presente cuando :other es :value.', + 'present_unless' => 'El campo :attribute debe estar presente a menos que :other sea :value.', + 'present_with' => 'El campo :attribute debe estar presente cuando :values está presente.', + 'present_with_all' => 'El campo :attribute debe estar presente cuando :values están presentes.', + 'prohibited' => 'El campo :attribute está prohibido.', + 'prohibited_if' => 'El campo :attribute está prohibido cuando :other es :value.', + 'prohibited_if_accepted' => 'El campo :attribute está prohibido cuando :other es aceptado.', + 'prohibited_if_declined' => 'El campo :attribute está prohibido cuando :other es rechazado.', + 'prohibited_unless' => 'El campo :attribute está prohibido a menos que :other esté en :values.', + 'prohibits' => 'El campo :attribute prohíbe que :other esté presente.', + 'regex' => 'El formato del campo :attribute no es válido.', + 'required' => 'El campo :attribute es obligatorio.', + 'required_array_keys' => 'El campo :attribute debe contener entradas para: :values.', + 'required_if' => 'El campo :attribute es obligatorio cuando :other es :value.', + 'required_if_accepted' => 'El campo :attribute es obligatorio cuando :other es aceptado.', + 'required_if_declined' => 'El campo :attribute es obligatorio cuando :other es rechazado.', + 'required_unless' => 'El campo :attribute es obligatorio a menos que :other esté en :values.', + 'required_with' => 'El campo :attribute es obligatorio cuando :values está presente.', + 'required_with_all' => 'El campo :attribute es obligatorio cuando :values están presentes.', + 'required_without' => 'El campo :attribute es obligatorio cuando :values no está presente.', + 'required_without_all' => 'El campo :attribute es obligatorio cuando ninguno de :values está presente.', + 'same' => 'El campo :attribute debe coincidir con :other.', + 'size' => [ + 'array' => 'El campo :attribute debe contener :size elementos.', + 'file' => 'El campo :attribute debe pesar :size kilobytes.', + 'numeric' => 'El campo :attribute debe ser :size.', + 'string' => 'El campo :attribute debe tener :size caracteres.', + ], + 'starts_with' => 'El campo :attribute debe comenzar con uno de los siguientes: :values.', + 'string' => 'El campo :attribute debe ser una cadena de texto.', + 'timezone' => 'El campo :attribute debe ser una zona horaria válida.', + 'unique' => 'El :attribute ya ha sido registrado.', + 'uploaded' => 'El :attribute no se pudo subir.', + 'uppercase' => 'El campo :attribute debe estar en mayúsculas.', + 'url' => 'El campo :attribute debe ser una URL válida.', + 'ulid' => 'El campo :attribute debe ser un ULID válido.', + 'uuid' => 'El campo :attribute debe ser un UUID válido.', + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + 'attributes' => [], +]; diff --git a/lang/php_en.json b/lang/php_en.json deleted file mode 100644 index 01ffbf10..00000000 --- a/lang/php_en.json +++ /dev/null @@ -1 +0,0 @@ -{"auth.failed":"These credentials do not match our records.","auth.password":"The provided password is incorrect.","auth.throttle":"Too many login attempts. Please try again in :seconds seconds.","pagination.previous":"« Previous","pagination.next":"Next »","passwords.reset":"Your password has been reset.","passwords.sent":"We have emailed your password reset link.","passwords.throttled":"Please wait before retrying.","passwords.token":"This password reset token is invalid.","passwords.user":"We can't find a user with that email address.","validation.accepted":"The :attribute field must be accepted.","validation.accepted_if":"The :attribute field must be accepted when :other is :value.","validation.active_url":"The :attribute field must be a valid URL.","validation.after":"The :attribute field must be a date after :date.","validation.after_or_equal":"The :attribute field must be a date after or equal to :date.","validation.alpha":"The :attribute field must only contain letters.","validation.alpha_dash":"The :attribute field must only contain letters, numbers, dashes, and underscores.","validation.alpha_num":"The :attribute field must only contain letters and numbers.","validation.any_of":"The :attribute field is invalid.","validation.array":"The :attribute field must be an array.","validation.ascii":"The :attribute field must only contain single-byte alphanumeric characters and symbols.","validation.before":"The :attribute field must be a date before :date.","validation.before_or_equal":"The :attribute field must be a date before or equal to :date.","validation.between.array":"The :attribute field must have between :min and :max items.","validation.between.file":"The :attribute field must be between :min and :max kilobytes.","validation.between.numeric":"The :attribute field must be between :min and :max.","validation.between.string":"The :attribute field must be between :min and :max characters.","validation.boolean":"The :attribute field must be true or false.","validation.can":"The :attribute field contains an unauthorized value.","validation.confirmed":"The :attribute field confirmation does not match.","validation.contains":"The :attribute field is missing a required value.","validation.current_password":"The password is incorrect.","validation.date":"The :attribute field must be a valid date.","validation.date_equals":"The :attribute field must be a date equal to :date.","validation.date_format":"The :attribute field must match the format :format.","validation.decimal":"The :attribute field must have :decimal decimal places.","validation.declined":"The :attribute field must be declined.","validation.declined_if":"The :attribute field must be declined when :other is :value.","validation.different":"The :attribute field and :other must be different.","validation.digits":"The :attribute field must be :digits digits.","validation.digits_between":"The :attribute field must be between :min and :max digits.","validation.dimensions":"The :attribute field has invalid image dimensions.","validation.distinct":"The :attribute field has a duplicate value.","validation.doesnt_contain":"The :attribute field must not contain any of the following: :values.","validation.doesnt_end_with":"The :attribute field must not end with one of the following: :values.","validation.doesnt_start_with":"The :attribute field must not start with one of the following: :values.","validation.email":"The :attribute field must be a valid email address.","validation.encoding":"The :attribute field must be encoded in :encoding.","validation.ends_with":"The :attribute field must end with one of the following: :values.","validation.enum":"The selected :attribute is invalid.","validation.exists":"The selected :attribute is invalid.","validation.extensions":"The :attribute field must have one of the following extensions: :values.","validation.file":"The :attribute field must be a file.","validation.filled":"The :attribute field must have a value.","validation.gt.array":"The :attribute field must have more than :value items.","validation.gt.file":"The :attribute field must be greater than :value kilobytes.","validation.gt.numeric":"The :attribute field must be greater than :value.","validation.gt.string":"The :attribute field must be greater than :value characters.","validation.gte.array":"The :attribute field must have :value items or more.","validation.gte.file":"The :attribute field must be greater than or equal to :value kilobytes.","validation.gte.numeric":"The :attribute field must be greater than or equal to :value.","validation.gte.string":"The :attribute field must be greater than or equal to :value characters.","validation.hex_color":"The :attribute field must be a valid hexadecimal color.","validation.image":"The :attribute field must be an image.","validation.in":"The selected :attribute is invalid.","validation.in_array":"The :attribute field must exist in :other.","validation.in_array_keys":"The :attribute field must contain at least one of the following keys: :values.","validation.integer":"The :attribute field must be an integer.","validation.ip":"The :attribute field must be a valid IP address.","validation.ipv4":"The :attribute field must be a valid IPv4 address.","validation.ipv6":"The :attribute field must be a valid IPv6 address.","validation.json":"The :attribute field must be a valid JSON string.","validation.list":"The :attribute field must be a list.","validation.lowercase":"The :attribute field must be lowercase.","validation.lt.array":"The :attribute field must have less than :value items.","validation.lt.file":"The :attribute field must be less than :value kilobytes.","validation.lt.numeric":"The :attribute field must be less than :value.","validation.lt.string":"The :attribute field must be less than :value characters.","validation.lte.array":"The :attribute field must not have more than :value items.","validation.lte.file":"The :attribute field must be less than or equal to :value kilobytes.","validation.lte.numeric":"The :attribute field must be less than or equal to :value.","validation.lte.string":"The :attribute field must be less than or equal to :value characters.","validation.mac_address":"The :attribute field must be a valid MAC address.","validation.max.array":"The :attribute field must not have more than :max items.","validation.max.file":"The :attribute field must not be greater than :max kilobytes.","validation.max.numeric":"The :attribute field must not be greater than :max.","validation.max.string":"The :attribute field must not be greater than :max characters.","validation.max_digits":"The :attribute field must not have more than :max digits.","validation.mimes":"The :attribute field must be a file of type: :values.","validation.mimetypes":"The :attribute field must be a file of type: :values.","validation.min.array":"The :attribute field must have at least :min items.","validation.min.file":"The :attribute field must be at least :min kilobytes.","validation.min.numeric":"The :attribute field must be at least :min.","validation.min.string":"The :attribute field must be at least :min characters.","validation.min_digits":"The :attribute field must have at least :min digits.","validation.missing":"The :attribute field must be missing.","validation.missing_if":"The :attribute field must be missing when :other is :value.","validation.missing_unless":"The :attribute field must be missing unless :other is :value.","validation.missing_with":"The :attribute field must be missing when :values is present.","validation.missing_with_all":"The :attribute field must be missing when :values are present.","validation.multiple_of":"The :attribute field must be a multiple of :value.","validation.not_in":"The selected :attribute is invalid.","validation.not_regex":"The :attribute field format is invalid.","validation.numeric":"The :attribute field must be a number.","validation.password.letters":"The :attribute field must contain at least one letter.","validation.password.mixed":"The :attribute field must contain at least one uppercase and one lowercase letter.","validation.password.numbers":"The :attribute field must contain at least one number.","validation.password.symbols":"The :attribute field must contain at least one symbol.","validation.password.uncompromised":"The given :attribute has appeared in a data leak. Please choose a different :attribute.","validation.present":"The :attribute field must be present.","validation.present_if":"The :attribute field must be present when :other is :value.","validation.present_unless":"The :attribute field must be present unless :other is :value.","validation.present_with":"The :attribute field must be present when :values is present.","validation.present_with_all":"The :attribute field must be present when :values are present.","validation.prohibited":"The :attribute field is prohibited.","validation.prohibited_if":"The :attribute field is prohibited when :other is :value.","validation.prohibited_if_accepted":"The :attribute field is prohibited when :other is accepted.","validation.prohibited_if_declined":"The :attribute field is prohibited when :other is declined.","validation.prohibited_unless":"The :attribute field is prohibited unless :other is in :values.","validation.prohibits":"The :attribute field prohibits :other from being present.","validation.regex":"The :attribute field format is invalid.","validation.required":"The :attribute field is required.","validation.required_array_keys":"The :attribute field must contain entries for: :values.","validation.required_if":"The :attribute field is required when :other is :value.","validation.required_if_accepted":"The :attribute field is required when :other is accepted.","validation.required_if_declined":"The :attribute field is required when :other is declined.","validation.required_unless":"The :attribute field is required unless :other is in :values.","validation.required_with":"The :attribute field is required when :values is present.","validation.required_with_all":"The :attribute field is required when :values are present.","validation.required_without":"The :attribute field is required when :values is not present.","validation.required_without_all":"The :attribute field is required when none of :values are present.","validation.same":"The :attribute field must match :other.","validation.size.array":"The :attribute field must contain :size items.","validation.size.file":"The :attribute field must be :size kilobytes.","validation.size.numeric":"The :attribute field must be :size.","validation.size.string":"The :attribute field must be :size characters.","validation.starts_with":"The :attribute field must start with one of the following: :values.","validation.string":"The :attribute field must be a string.","validation.timezone":"The :attribute field must be a valid timezone.","validation.unique":"The :attribute has already been taken.","validation.uploaded":"The :attribute failed to upload.","validation.uppercase":"The :attribute field must be uppercase.","validation.url":"The :attribute field must be a valid URL.","validation.ulid":"The :attribute field must be a valid ULID.","validation.uuid":"The :attribute field must be a valid UUID.","validation.custom.attribute-name.rule-name":"custom-message","accounts.title":"Connections","accounts.page_title":"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.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.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.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.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.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","hashtags.title":"Hashtags","hashtags.description":"Create hashtag groups to quickly add to your posts","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.new_label":"New Label","labels.no_labels_yet":"No labels yet","labels.create_first_label":"Create your first label","labels.create.title":"Create Label","labels.create.description":"Give your label a name and pick a color","labels.create.name":"Name","labels.create.name_placeholder":"Enter label name...","labels.create.color":"Color","labels.create.submit":"Create Label","labels.create.submitting":"Creating...","labels.edit.title":"Edit Label","labels.edit.description":"Update the name and color for this label","labels.edit.name":"Name","labels.edit.name_placeholder":"Enter label name...","labels.edit.color":"Color","labels.edit.submit":"Save Changes","labels.edit.submitting":"Saving...","labels.delete.title":"Delete Label","labels.delete.description":"Are you sure you want to delete this label? This action cannot be undone.","labels.delete.confirm":"Delete","labels.delete.cancel":"Cancel","labels.flash.created":"Label created successfully!","labels.flash.updated":"Label updated successfully!","labels.flash.deleted":"Label deleted successfully!","mail.workspace_connections_disconnected.subject":"{1} :count account needs to be reconnected in :workspace|[2,*] :count accounts need to be reconnected in :workspace","mail.workspace_connections_disconnected.title":"Accounts Need Reconnection","mail.workspace_connections_disconnected.intro":"The following social accounts in your :workspace workspace have been disconnected and need to be reconnected:","mail.workspace_connections_disconnected.reasons_title":"This may have happened because:","mail.workspace_connections_disconnected.reason_expired":"Access tokens expired","mail.workspace_connections_disconnected.reason_revoked":"You revoked access to TryPost on the platform","mail.workspace_connections_disconnected.reason_changed":"The platform changed their authentication requirements","mail.workspace_connections_disconnected.reconnect_cta":"Please reconnect these accounts to continue scheduling and publishing posts.","mail.workspace_connections_disconnected.button":"Reconnect Accounts","posts.title":"Posts","posts.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.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","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.billing":"Billing","settings.profile.title":"Profile settings","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.heading":"Workspace settings","settings.workspace.description":"Update your workspace name, logo, and timezone","settings.workspace.logo":"Logo","settings.workspace.name":"Name","settings.workspace.name_placeholder":"My Workspace","settings.workspace.timezone":"Timezone","settings.workspace.save":"Save","settings.workspace.saved":"Saved.","settings.members.title":"Members","settings.members.heading":"Team members","settings.members.description":"Manage members and invites for this workspace","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.invite.cancel_confirm":"Are you sure you want to cancel this 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.list.remove_confirm":"Are you sure you want to remove this member?","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.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!","sidebar.workspaces":"Workspaces","sidebar.select_workspace":"Select workspace","sidebar.create_workspace":"Create workspace","sidebar.create_post":"Create post","sidebar.profile":"Profile","sidebar.log_out":"Log out","sidebar.workspace":"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.support.discord":"Discord","sidebar.support.share_feedback":"Share feedback","sidebar.support.last_updates":"Last Updates","sidebar.support.docs":"Docs"} \ No newline at end of file diff --git a/lang/php_pt-br.json b/lang/php_pt-br.json deleted file mode 100644 index 62fe7bac..00000000 --- a/lang/php_pt-br.json +++ /dev/null @@ -1 +0,0 @@ -{"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.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.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.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.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.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","hashtags.title":"Hashtags","hashtags.description":"Crie grupos de hashtags para adicionar rapidamente aos seus posts","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.new_label":"Nova Etiqueta","labels.no_labels_yet":"Nenhuma etiqueta ainda","labels.create_first_label":"Crie sua primeira etiqueta","labels.create.title":"Criar Etiqueta","labels.create.description":"Dê um nome e escolha uma cor para sua etiqueta","labels.create.name":"Nome","labels.create.name_placeholder":"Digite o nome da etiqueta...","labels.create.color":"Cor","labels.create.submit":"Criar Etiqueta","labels.create.submitting":"Criando...","labels.edit.title":"Editar Etiqueta","labels.edit.description":"Atualize o nome e a cor desta etiqueta","labels.edit.name":"Nome","labels.edit.name_placeholder":"Digite o nome da etiqueta...","labels.edit.color":"Cor","labels.edit.submit":"Salvar Alterações","labels.edit.submitting":"Salvando...","labels.delete.title":"Excluir Etiqueta","labels.delete.description":"Tem certeza que deseja excluir esta etiqueta? Esta ação não pode ser desfeita.","labels.delete.confirm":"Excluir","labels.delete.cancel":"Cancelar","labels.flash.created":"Etiqueta criada com sucesso!","labels.flash.updated":"Etiqueta atualizada com sucesso!","labels.flash.deleted":"Etiqueta excluída com sucesso!","mail.workspace_connections_disconnected.subject":"{1} :count conta precisa ser reconectada em :workspace|[2,*] :count contas precisam ser reconectadas em :workspace","mail.workspace_connections_disconnected.title":"Contas Precisam ser Reconectadas","mail.workspace_connections_disconnected.intro":"As seguintes contas de redes sociais no seu workspace :workspace foram desconectadas e precisam ser reconectadas:","mail.workspace_connections_disconnected.reasons_title":"Isso pode ter acontecido porque:","mail.workspace_connections_disconnected.reason_expired":"Os tokens de acesso expiraram","mail.workspace_connections_disconnected.reason_revoked":"Você revogou o acesso ao TryPost na plataforma","mail.workspace_connections_disconnected.reason_changed":"A plataforma mudou os requisitos de autenticação","mail.workspace_connections_disconnected.reconnect_cta":"Por favor, reconecte essas contas para continuar agendando e publicando posts.","mail.workspace_connections_disconnected.button":"Reconectar Contas","pagination.previous":"« Anterior","pagination.next":"Próximo »","passwords.reset":"Sua senha foi redefinida.","passwords.sent":"Enviamos o link de redefinição de senha por e-mail.","passwords.throttled":"Por favor, aguarde antes de tentar novamente.","passwords.token":"Este token de redefinição de senha é inválido.","passwords.user":"Não conseguimos encontrar um usuário com esse endereço de e-mail.","posts.title":"Posts","posts.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.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":"Etiquetas","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","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.billing":"Faturamento","settings.profile.title":"Configurações do 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.heading":"Configurações do workspace","settings.workspace.description":"Atualize o nome, logo e fuso horário do workspace","settings.workspace.logo":"Logo","settings.workspace.name":"Nome","settings.workspace.name_placeholder":"Meu Workspace","settings.workspace.timezone":"Fuso horário","settings.workspace.save":"Salvar","settings.workspace.saved":"Salvo.","settings.members.title":"Membros","settings.members.heading":"Membros da equipe","settings.members.description":"Gerencie membros e convites deste workspace","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.invite.cancel_confirm":"Tem certeza que deseja cancelar este 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.list.remove_confirm":"Tem certeza que deseja remover este membro?","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.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!","sidebar.select_workspace":"Selecionar workspace","sidebar.create_workspace":"Criar workspace","sidebar.create_post":"Novo post","sidebar.profile":"Perfil","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.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"} \ No newline at end of file diff --git a/lang/pt-br/settings.php b/lang/pt-br/settings.php index e128010f..51ed7e98 100644 --- a/lang/pt-br/settings.php +++ b/lang/pt-br/settings.php @@ -14,6 +14,8 @@ 'profile' => [ 'title' => 'Configurações do perfil', + 'photo_heading' => 'Foto do perfil', + 'photo_description' => 'Envie uma foto de perfil', 'heading' => 'Informações do perfil', 'description' => 'Atualize seu nome e endereço de e-mail', 'avatar' => 'Avatar', @@ -56,14 +58,16 @@ 'workspace' => [ 'title' => 'Configurações do workspace', - 'heading' => 'Configurações do workspace', - 'description' => 'Atualize o nome, logo e fuso horário do workspace', - 'logo' => 'Logo', + 'logo_heading' => 'Logo do workspace', + 'logo_description' => 'Envie um logo para o workspace', + 'heading' => 'Nome do workspace', + 'description' => 'Atualize o nome e fuso horário do workspace', + 'members_heading' => 'Membros', + 'members_description' => 'Gerencie membros e convites do workspace', 'name' => 'Nome', 'name_placeholder' => 'Meu Workspace', 'timezone' => 'Fuso horário', 'save' => 'Salvar', - 'saved' => 'Salvo.', ], 'members' => [ @@ -71,6 +75,9 @@ 'heading' => 'Membros da equipe', 'description' => 'Gerencie membros e convites deste workspace', + 'cancel' => 'Cancelar', + 'remove' => 'Remover', + 'invite' => [ 'title' => 'Convidar Membro', 'description' => 'Envie um convite por e-mail para adicionar colaboradores', @@ -79,7 +86,6 @@ 'role' => 'Função', 'role_placeholder' => 'Selecione uma função', 'submit' => 'Enviar Convite', - 'cancel_confirm' => 'Tem certeza que deseja cancelar este convite?', ], 'pending' => [ @@ -92,7 +98,18 @@ 'title' => 'Membros', 'description' => 'Pessoas com acesso a este workspace', 'empty' => 'Nenhum membro além do proprietário', - 'remove_confirm' => 'Tem certeza que deseja remover este membro?', + ], + + 'remove_modal' => [ + 'title' => 'Remover membro', + 'description' => 'Tem certeza que deseja remover este membro do workspace? Ele perderá acesso a todos os recursos do workspace.', + 'action' => 'Remover membro', + ], + + 'cancel_invite_modal' => [ + 'title' => 'Cancelar convite', + 'description' => 'Tem certeza que deseja cancelar este convite?', + 'action' => 'Cancelar convite', ], 'roles' => [ @@ -119,5 +136,7 @@ 'workspace_updated' => 'Configurações atualizadas com sucesso!', 'photo_updated' => 'Foto atualizada com sucesso!', 'photo_deleted' => 'Foto removida com sucesso!', + 'logo_updated' => 'Logo enviado com sucesso!', + 'logo_deleted' => 'Logo removido com sucesso!', ], ]; diff --git a/resources/js/components/LanguageCombobox.vue b/resources/js/components/LanguageCombobox.vue deleted file mode 100644 index 1bc2984d..00000000 --- a/resources/js/components/LanguageCombobox.vue +++ /dev/null @@ -1,97 +0,0 @@ - - - diff --git a/resources/js/components/UserMenuContent.vue b/resources/js/components/UserMenuContent.vue index faa49d68..f95e676f 100644 --- a/resources/js/components/UserMenuContent.vue +++ b/resources/js/components/UserMenuContent.vue @@ -30,9 +30,8 @@ import { updateLanguage } from '@/actions/App/Http/Controllers/App/Settings/Prof import type { User } from '@/types'; interface Language { - id: string; - name: string; code: string; + name: string; } type Props = { @@ -43,24 +42,22 @@ defineProps(); const page = usePage(); const languages = computed(() => page.props.languages as Language[]); -const currentLanguage = computed(() => languages.value?.find((l: Language) => l.id === page.props.auth.user?.language_id)); +const currentLanguage = computed(() => languages.value?.find((l: Language) => l.code === page.props.auth.user?.locale)); const { appearance, updateAppearance } = useAppearance(); -const switchLanguage = (languageId: string) => { - const language = languages.value.find((l: Language) => l.id === languageId); - const languageCode = language?.code || 'en'; - const previousLanguageCode = currentLanguage.value?.code || 'en'; +const switchLanguage = (code: string) => { + const previousCode = currentLanguage.value?.code || 'en'; - loadLanguageAsync(languageCode); - dayjs.locale(languageCode.toLowerCase()); + loadLanguageAsync(code); + dayjs.locale(code.toLowerCase()); - router.patch(updateLanguage.url(), { language_id: languageId }, { + router.patch(updateLanguage.url(), { locale: code }, { preserveScroll: true, preserveState: false, onError: () => { - loadLanguageAsync(previousLanguageCode); - dayjs.locale(previousLanguageCode.toLowerCase()); + loadLanguageAsync(previousCode); + dayjs.locale(previousCode.toLowerCase()); }, }); }; @@ -136,9 +133,9 @@ const handleLogout = () => { {{ language.name }} diff --git a/resources/js/components/ui/table/Table.vue b/resources/js/components/ui/table/Table.vue new file mode 100644 index 00000000..0d0cd9b4 --- /dev/null +++ b/resources/js/components/ui/table/Table.vue @@ -0,0 +1,16 @@ + + + diff --git a/resources/js/components/ui/table/TableBody.vue b/resources/js/components/ui/table/TableBody.vue new file mode 100644 index 00000000..d14a2d32 --- /dev/null +++ b/resources/js/components/ui/table/TableBody.vue @@ -0,0 +1,17 @@ + + + diff --git a/resources/js/components/ui/table/TableCaption.vue b/resources/js/components/ui/table/TableCaption.vue new file mode 100644 index 00000000..36300845 --- /dev/null +++ b/resources/js/components/ui/table/TableCaption.vue @@ -0,0 +1,17 @@ + + + diff --git a/resources/js/components/ui/table/TableCell.vue b/resources/js/components/ui/table/TableCell.vue new file mode 100644 index 00000000..d6e9ed24 --- /dev/null +++ b/resources/js/components/ui/table/TableCell.vue @@ -0,0 +1,22 @@ + + + diff --git a/resources/js/components/ui/table/TableFooter.vue b/resources/js/components/ui/table/TableFooter.vue new file mode 100644 index 00000000..29e0ce9a --- /dev/null +++ b/resources/js/components/ui/table/TableFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/resources/js/components/ui/table/TableHead.vue b/resources/js/components/ui/table/TableHead.vue new file mode 100644 index 00000000..f83efe56 --- /dev/null +++ b/resources/js/components/ui/table/TableHead.vue @@ -0,0 +1,17 @@ + + + diff --git a/resources/js/components/ui/table/TableHeader.vue b/resources/js/components/ui/table/TableHeader.vue new file mode 100644 index 00000000..b4ab5cfb --- /dev/null +++ b/resources/js/components/ui/table/TableHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/resources/js/components/ui/table/TableRow.vue b/resources/js/components/ui/table/TableRow.vue new file mode 100644 index 00000000..8f1d172f --- /dev/null +++ b/resources/js/components/ui/table/TableRow.vue @@ -0,0 +1,17 @@ + + + diff --git a/resources/js/components/ui/table/index.ts b/resources/js/components/ui/table/index.ts new file mode 100644 index 00000000..13adbc44 --- /dev/null +++ b/resources/js/components/ui/table/index.ts @@ -0,0 +1,8 @@ +export { default as Table } from "./Table.vue" +export { default as TableBody } from "./TableBody.vue" +export { default as TableCaption } from "./TableCaption.vue" +export { default as TableCell } from "./TableCell.vue" +export { default as TableFooter } from "./TableFooter.vue" +export { default as TableHead } from "./TableHead.vue" +export { default as TableHeader } from "./TableHeader.vue" +export { default as TableRow } from "./TableRow.vue" diff --git a/resources/js/layouts/settings/Layout.vue b/resources/js/layouts/settings/Layout.vue index e14eb3db..e742de92 100644 --- a/resources/js/layouts/settings/Layout.vue +++ b/resources/js/layouts/settings/Layout.vue @@ -3,10 +3,8 @@ import { Link, usePage } from '@inertiajs/vue3'; import { trans } from 'laravel-vue-i18n'; import { computed } from 'vue'; -import Heading from '@/components/Heading.vue'; import { useActiveUrl } from '@/composables/useActiveUrl'; import { toUrl } from '@/lib/utils'; -import { members } from '@/routes/app'; import { index as apiKeys } from '@/routes/app/api-keys'; import { index as billing } from '@/routes/app/billing'; import { edit as editProfile } from '@/routes/app/profile'; @@ -36,10 +34,6 @@ const navItems = computed(() => { title: trans('settings.nav.workspace'), href: workspaceSettings(), }, - { - title: trans('settings.nav.members'), - href: members(), - }, { title: 'API Keys', href: apiKeys(), @@ -61,25 +55,21 @@ const { urlIsActive } = useActiveUrl(); \ No newline at end of file diff --git a/resources/js/pages/settings/Profile.vue b/resources/js/pages/settings/Profile.vue index aca10492..680c4367 100644 --- a/resources/js/pages/settings/Profile.vue +++ b/resources/js/pages/settings/Profile.vue @@ -11,6 +11,7 @@ import PhotoUpload from '@/components/PhotoUpload.vue'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { Separator } from '@/components/ui/separator'; import AppLayout from '@/layouts/AppLayout.vue'; import SettingsLayout from '@/layouts/settings/Layout.vue'; import { edit, uploadPhoto, deletePhoto } from '@/routes/app/profile'; @@ -42,89 +43,93 @@ const breadcrumbItems = computed(() => [

{{ $t('settings.profile.title') }}

-
-
- +
+ + +
+ + + +
+ + +
- - {{ $t('settings.profile.name') }} + +
- -
- - - -
+
+ + + +
-
- - - -
- -
-

- {{ $t('settings.profile.email_unverified') }} - - {{ $t('settings.profile.resend_verification') }} - -

- -
+

+ {{ $t('settings.profile.email_unverified') }} + - {{ $t('settings.profile.verification_sent') }} -

-
+ {{ $t('settings.profile.resend_verification') }} + +

- -
-
+ {{ $t('settings.profile.verification_sent') }} +
+
-
- - + + + + + +
diff --git a/resources/js/pages/settings/Workspace.vue b/resources/js/pages/settings/Workspace.vue index cc38751b..6b2158df 100644 --- a/resources/js/pages/settings/Workspace.vue +++ b/resources/js/pages/settings/Workspace.vue @@ -1,19 +1,55 @@ diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index 00f6fe8c..5afe4798 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -34,11 +34,13 @@ export interface User { id: number; name: string; email: string; - avatar?: string; + has_photo: boolean; + photo_url: string | null; + locale: string; email_verified_at: string | null; created_at: string; updated_at: string; - [key: string]: unknown; // This allows for additional properties... + [key: string]: unknown; } export type BreadcrumbItemType = BreadcrumbItem; diff --git a/routes/app.php b/routes/app.php index aaaa9787..a1799a6e 100644 --- a/routes/app.php +++ b/routes/app.php @@ -136,7 +136,7 @@ function () { Route::delete('medias/{modelId}/{media}', [MediaController::class, 'destroy'])->name('app.medias.destroy'); // Members - Route::get('settings/members', [WorkspaceInviteController::class, 'index'])->name('app.members'); + Route::get('settings/members', fn () => redirect()->route('app.workspace.settings'))->name('app.members'); Route::post('settings/members/invites', [WorkspaceInviteController::class, 'store'])->name('app.invites.store'); Route::delete('settings/members/invites/{invite}', [WorkspaceInviteController::class, 'destroy'])->name('app.invites.destroy'); Route::delete('settings/members/{user}', [WorkspaceInviteController::class, 'removeMember'])->name('app.members.remove'); diff --git a/routes/channels.php b/routes/channels.php index c2d2889a..fb56c457 100644 --- a/routes/channels.php +++ b/routes/channels.php @@ -1,5 +1,7 @@ create(['code' => 'en']); $this->owner = User::factory()->create(['setup' => Setup::Completed]); $this->workspace = Workspace::factory()->create(['user_id' => $this->owner->id]); }); diff --git a/tests/Feature/Api/ApiKeyApiTest.php b/tests/Feature/Api/ApiKeyApiTest.php index c9731997..b0d61359 100644 --- a/tests/Feature/Api/ApiKeyApiTest.php +++ b/tests/Feature/Api/ApiKeyApiTest.php @@ -1,5 +1,7 @@ shouldReceive('publish')->andThrow(new TokenExpiredException('Token expired', 401)); + $publisher->shouldReceive('publish')->andThrow(new TokenExpiredException('Token expired', '401')); $this->app->instance(LinkedInPublisher::class, $publisher); diff --git a/tests/Feature/Listeners/StripeEventListenerTest.php b/tests/Feature/Listeners/StripeEventListenerTest.php index 8e9f00ab..fbc3f72a 100644 --- a/tests/Feature/Listeners/StripeEventListenerTest.php +++ b/tests/Feature/Listeners/StripeEventListenerTest.php @@ -1,5 +1,7 @@ refresh()->email_verified_at)->not->toBeNull(); }); -test('user can update their language', function () { +test('user can update their locale', function () { $user = User::factory()->create(); - $newLanguage = Language::factory()->create(); $response = $this ->actingAs($user) ->from(route('app.posts.index')) ->patch(route('app.profile.language'), [ - 'language_id' => $newLanguage->id, + 'locale' => 'es', ]); $response ->assertSessionHasNoErrors() ->assertRedirect(route('app.posts.index')); - expect($user->refresh()->language_id)->toBe($newLanguage->id); + expect($user->refresh()->locale)->toBe('es'); }); -test('user cannot update language with invalid language id', function () { +test('user cannot update locale with invalid code', function () { $user = User::factory()->create(); $response = $this ->actingAs($user) ->patch(route('app.profile.language'), [ - 'language_id' => '00000000-0000-0000-0000-000000000000', + 'locale' => 'invalid', ]); - $response->assertSessionHasErrors('language_id'); + $response->assertSessionHasErrors('locale'); }); test('user can delete their account', function () { diff --git a/tests/Feature/Social/BlueskyControllerTest.php b/tests/Feature/Social/BlueskyControllerTest.php index 6c79192d..d95f36b9 100644 --- a/tests/Feature/Social/BlueskyControllerTest.php +++ b/tests/Feature/Social/BlueskyControllerTest.php @@ -1,5 +1,7 @@ user = User::factory()->create(['setup' => Setup::Completed]); @@ -162,7 +165,7 @@ $response->assertRedirect(route('login')); }); -test('workspace settings shows settings page', function () { +test('workspace settings shows settings page with members and invitations', function () { $response = $this->actingAs($this->user)->get(route('app.workspace.settings')); $response->assertOk(); @@ -170,6 +173,8 @@ ->component('settings/Workspace', false) ->has('workspace') ->has('timezones') + ->has('members') + ->has('invitations') ); }); @@ -222,6 +227,85 @@ $response->assertSessionHasErrors('timezone'); }); +// Logo upload tests +test('upload workspace logo requires authentication', function () { + $response = $this->post(route('app.workspace.upload-logo'), [ + 'photo' => UploadedFile::fake()->image('logo.jpg'), + ]); + + $response->assertRedirect(route('login')); +}); + +test('upload workspace logo succeeds with valid image', function () { + $response = $this->actingAs($this->user)->post(route('app.workspace.upload-logo'), [ + 'photo' => UploadedFile::fake()->image('logo.jpg', 200, 200), + ]); + + $response->assertRedirect(); + + $this->workspace->refresh(); + expect($this->workspace->has_logo)->toBeTrue(); + expect($this->workspace->logo_url)->not->toBeNull(); +}); + +test('upload workspace logo validates file is an image', function () { + $response = $this->actingAs($this->user)->post(route('app.workspace.upload-logo'), [ + 'photo' => UploadedFile::fake()->create('document.pdf', 100), + ]); + + $response->assertSessionHasErrors('photo'); +}); + +test('upload workspace logo validates max size', function () { + $response = $this->actingAs($this->user)->post(route('app.workspace.upload-logo'), [ + 'photo' => UploadedFile::fake()->image('logo.jpg')->size(3000), + ]); + + $response->assertSessionHasErrors('photo'); +}); + +test('upload workspace logo requires authorization', function () { + $otherUser = User::factory()->create(['setup' => Setup::Completed]); + + $response = $this->actingAs($otherUser)->post(route('app.workspace.upload-logo'), [ + 'photo' => UploadedFile::fake()->image('logo.jpg'), + ]); + + $response->assertForbidden(); +}); + +test('delete workspace logo requires authentication', function () { + $response = $this->delete(route('app.workspace.delete-logo')); + + $response->assertRedirect(route('login')); +}); + +test('delete workspace logo succeeds', function () { + // Upload first + $this->actingAs($this->user)->post(route('app.workspace.upload-logo'), [ + 'photo' => UploadedFile::fake()->image('logo.jpg', 200, 200), + ]); + + $this->workspace->refresh(); + expect($this->workspace->has_logo)->toBeTrue(); + + // Delete + $response = $this->actingAs($this->user)->delete(route('app.workspace.delete-logo')); + + $response->assertRedirect(); + + $this->workspace->refresh(); + expect($this->workspace->has_logo)->toBeFalse(); +}); + +test('delete workspace logo requires authorization', function () { + $otherUser = User::factory()->create(['setup' => Setup::Completed]); + + $response = $this->actingAs($otherUser)->delete(route('app.workspace.delete-logo')); + + $response->assertForbidden(); +}); + // Destroy tests test('destroy workspace requires authentication', function () { $response = $this->delete(route('app.workspaces.destroy', $this->workspace)); diff --git a/tests/Feature/WorkspaceHashtagControllerTest.php b/tests/Feature/WorkspaceHashtagControllerTest.php index 3f41a79d..79c45f3d 100644 --- a/tests/Feature/WorkspaceHashtagControllerTest.php +++ b/tests/Feature/WorkspaceHashtagControllerTest.php @@ -1,5 +1,7 @@ assertRedirect(route('login')); }); -test('members index shows members and invites', function () { +test('members index redirects to workspace settings', function () { + $response = $this->actingAs($this->user)->get(route('app.members')); + + $response->assertRedirect(route('app.workspace.settings')); +}); + +test('workspace settings shows members and invites', function () { $invite = WorkspaceInvite::factory()->create([ 'workspace_id' => $this->workspace->id, ]); - $response = $this->actingAs($this->user)->get(route('app.members')); + $response = $this->actingAs($this->user)->get(route('app.workspace.settings')); $response->assertOk(); $response->assertInertia(fn ($page) => $page - ->component('settings/Members', false) + ->component('settings/Workspace', false) ->has('workspace') - ->has('invites') ->has('members') - ->has('owner') - ->has('roles') + ->has('invitations') + ->has('timezones') ); }); -test('members index redirects if no workspace', function () { - $this->user->update(['current_workspace_id' => null]); - - $response = $this->actingAs($this->user)->get(route('app.members')); - - $response->assertRedirect(route('app.workspaces.create')); -}); - // Store invite tests test('store invite requires authentication', function () { $response = $this->post(route('app.invites.store'), [ diff --git a/tests/Feature/WorkspaceLabelControllerTest.php b/tests/Feature/WorkspaceLabelControllerTest.php index 3783d2c4..8021383a 100644 --- a/tests/Feature/WorkspaceLabelControllerTest.php +++ b/tests/Feature/WorkspaceLabelControllerTest.php @@ -1,5 +1,7 @@ toBeTrue(); }); diff --git a/tests/Unit/HelpersTest.php b/tests/Unit/HelpersTest.php index 1c7dc27b..0976480c 100644 --- a/tests/Unit/HelpersTest.php +++ b/tests/Unit/HelpersTest.php @@ -1,5 +1,7 @@ create(); - $user = User::factory()->create(['language_id' => $language->id]); - - expect($language->users)->toHaveCount(1); - expect($language->users->first()->id)->toBe($user->id); -}); - -test('language has fillable attributes', function () { - $language = Language::factory()->create([ - 'name' => 'Portuguese', - 'code' => 'pt-br', - ]); - - expect($language->name)->toBe('Portuguese'); - expect($language->code)->toBe('pt-br'); -}); diff --git a/tests/Unit/Models/MediaTest.php b/tests/Unit/Models/MediaTest.php index e33f834a..1b154bc3 100644 --- a/tests/Unit/Models/MediaTest.php +++ b/tests/Unit/Models/MediaTest.php @@ -1,5 +1,7 @@ create(['name' => 'Test User']); - $avatar = $user->avatar; - - expect($avatar['url'])->toContain('dicebear'); - expect($avatar['media_id'])->toBeNull(); + expect($user->has_photo)->toBeFalse(); + expect($user->photo_url)->toBeNull(); }); -test('user avatar attribute returns media url when exists', function () { +test('user has_photo returns true when avatar exists', function () { $user = User::factory()->create(['name' => 'Test User']); $file = UploadedFile::fake()->image('avatar.jpg', 100, 100); - $media = $user->addMedia($file, 'avatar'); + $user->addMedia($file, 'avatar'); $user->refresh(); - $avatar = $user->avatar; - expect($avatar['media_id'])->toBe($media->id); + expect($user->has_photo)->toBeTrue(); + expect($user->photo_url)->not->toBeNull(); }); diff --git a/tests/Unit/Traits/HasWorkspaceTest.php b/tests/Unit/Traits/HasWorkspaceTest.php index 7db4c01c..8e593928 100644 --- a/tests/Unit/Traits/HasWorkspaceTest.php +++ b/tests/Unit/Traits/HasWorkspaceTest.php @@ -1,5 +1,7 @@