diff --git a/app/Enums/PostPlatform/ContentType.php b/app/Enums/PostPlatform/ContentType.php index e68193aad..12138cf57 100644 --- a/app/Enums/PostPlatform/ContentType.php +++ b/app/Enums/PostPlatform/ContentType.php @@ -476,6 +476,14 @@ public function supportsAltText(): bool return $this->platform()->supportsAltText(); } + /** + * Whether the editor offers the Alt Text tab for videos of this type. + */ + public function supportsVideoAltText(): bool + { + return $this->platform()->supportsVideoAltText(); + } + /** * Whether the editor offers Tag People (Instagram posts only, not stories or reels). */ @@ -562,6 +570,7 @@ public function mediaRules(): array 'auto_fits_image' => $this->autoFitsImage(), 'crop_presets' => $this->cropPresets(), 'supports_alt_text' => $this->supportsAltText(), + 'supports_video_alt_text' => $this->supportsVideoAltText(), 'supports_user_tags' => $this->supportsUserTags(), 'supports_video_cover' => $this->supportsVideoCover(), 'platform_label' => $this->platform()->label(), @@ -643,6 +652,7 @@ public function toListingArray(): array 'image_max_width', 'image_max_height', 'supports_alt_text', + 'supports_video_alt_text', 'supports_user_tags', 'supports_video_cover', ])), diff --git a/app/Enums/SocialAccount/Platform.php b/app/Enums/SocialAccount/Platform.php index 20e3ff754..1c703611d 100644 --- a/app/Enums/SocialAccount/Platform.php +++ b/app/Enums/SocialAccount/Platform.php @@ -204,6 +204,19 @@ public function supportsAltText(): bool return $this->altTextMaxLength() !== null; } + /** + * Whether the platform's API accepts an accessibility description on + * published VIDEO media too — Mastodon takes it as the media + * `description`, Bluesky as the video embed's `alt`. + */ + public function supportsVideoAltText(): bool + { + return match ($this) { + self::Bluesky, self::Mastodon => true, + default => false, + }; + } + /** * Hard cap (in characters) the platform's API will accept. Going over this * means the post can't be published. Values are the documented API maxes: diff --git a/app/Http/Controllers/Auth/InstagramController.php b/app/Http/Controllers/Auth/InstagramController.php index 1d6f62297..7fd113e4a 100644 --- a/app/Http/Controllers/Auth/InstagramController.php +++ b/app/Http/Controllers/Auth/InstagramController.php @@ -24,6 +24,8 @@ class InstagramController extends SocialController 'instagram_business_basic', 'instagram_business_content_publish', 'instagram_business_manage_insights', + // first comment after publish (FirstCommentPoster) + 'instagram_business_manage_comments', ]; public function connect(Request $request): Response diff --git a/app/Http/Controllers/Auth/InstagramFacebookController.php b/app/Http/Controllers/Auth/InstagramFacebookController.php index 222b23f11..0eb1016f4 100644 --- a/app/Http/Controllers/Auth/InstagramFacebookController.php +++ b/app/Http/Controllers/Auth/InstagramFacebookController.php @@ -44,6 +44,8 @@ class InstagramFacebookController extends MetaController 'instagram_basic', 'instagram_content_publish', 'instagram_manage_insights', + // first comment after publish (FirstCommentPoster) + 'instagram_manage_comments', ]; public function connect(Request $request): Response diff --git a/app/Jobs/PublishToSocialPlatform.php b/app/Jobs/PublishToSocialPlatform.php index 90110b27b..ede1a2902 100644 --- a/app/Jobs/PublishToSocialPlatform.php +++ b/app/Jobs/PublishToSocialPlatform.php @@ -30,6 +30,7 @@ use App\Services\Social\Telegram\TelegramPublisher; use App\Services\Social\ThreadsPublisher; use App\Services\Social\TikTokPublisher; +use App\Services\Social\FirstCommentPoster; use App\Services\Social\XPublisher; use App\Services\Social\YouTubePublisher; use App\Support\Social\GoogleBusinessDerivativeCleaner; @@ -130,6 +131,15 @@ public function handle(): void $result = $publisher->publish($this->post); $this->recordPublishResult($result); + + // The post is live; a failed first comment must never fail it — + // FirstCommentPoster logs and swallows its own errors. Posts + // held in pending review (Google Business) get no comment. + if ($this->post->publish_status === PublishStatus::Published + && ($externalId = (string) data_get($result, 'id')) !== '') { + app(FirstCommentPoster::class)->post($this->post, $externalId); + } + break; } catch (PlatformUnavailableException $e) { $this->rescheduleForRetry($e); diff --git a/app/Services/Social/BlueskyPublisher.php b/app/Services/Social/BlueskyPublisher.php index e1e045c2a..bc03b47df 100644 --- a/app/Services/Social/BlueskyPublisher.php +++ b/app/Services/Social/BlueskyPublisher.php @@ -203,10 +203,20 @@ private function mediaEmbed(SocialAccount $account, string $service, Collection ); } - return $videoBlob ? [ + if (! $videoBlob) { + return null; + } + + $embed = [ '$type' => BlueskyLexicon::EMBED_VIDEO, 'video' => $videoBlob, - ] : null; + ]; + + if (($alt = $video->altTextFor(Platform::Bluesky)) !== null) { + $embed['alt'] = $alt; + } + + return $embed; } /** diff --git a/app/Services/Social/FacebookPublisher.php b/app/Services/Social/FacebookPublisher.php index 1a0113bcb..957b845e9 100644 --- a/app/Services/Social/FacebookPublisher.php +++ b/app/Services/Social/FacebookPublisher.php @@ -37,6 +37,12 @@ class FacebookPublisher private string $baseUrl; + /** + * Facebook place ID from the platform row's `meta.location_id` — tags feed + * and photo posts with a location. Stories do not accept it. + */ + private ?string $placeId = null; + public function __construct() { $this->baseUrl = config('trypost.platforms.facebook.graph_api'); @@ -53,6 +59,7 @@ public function publish(Post $post): array $pageId = $account->platform_user_id; $accessToken = $account->access_token; $content = $this->sanitizedContent($post); + $this->placeId = trim((string) data_get($post->meta, 'location_id')) ?: null; $media = $post->mediaItems; $contentType = $post->content_type; @@ -149,11 +156,11 @@ private function postTextWithLink(string $pageId, string $accessToken, string $c */ private function postTextToFeed(string $pageId, string $accessToken, string $content, ?string $link, bool $reportFailure = true): Response { - return $this->postToGraph("{$pageId}/feed", [ + return $this->postToGraph("{$pageId}/feed", $this->withPlace([ 'message' => $content, 'access_token' => $accessToken, ...$this->optionalField('link', $link), - ], 'text post', $reportFailure); + ]), 'text post', $reportFailure); } /** @@ -161,12 +168,12 @@ private function postTextToFeed(string $pageId, string $accessToken, string $con */ private function publishSingleImagePost(string $pageId, string $accessToken, ?string $content, MediaItem $media): array { - $response = $this->postToGraph("{$pageId}/photos", [ + $response = $this->postToGraph("{$pageId}/photos", $this->withPlace([ 'url' => $media->url, 'access_token' => $accessToken, ...$this->optionalField('message', $content), ...$this->altText($media), - ], 'single image post'); + ]), 'single image post'); $data = $response->json(); @@ -196,13 +203,13 @@ private function publishMultiImagePost(string $pageId, string $accessToken, ?str ); } - $response = $this->postToGraph("{$pageId}/feed", [ + $response = $this->postToGraph("{$pageId}/feed", $this->withPlace([ 'access_token' => $accessToken, ...$this->optionalField('message', $content), ...$photoIds ->mapWithKeys(fn (string $photoId, int $index): array => ["attached_media[{$index}]" => json_encode(['media_fbid' => $photoId])]) ->all(), - ], 'multi-image post'); + ]), 'multi-image post'); return $this->feedPostResult(data_get($response->json(), 'id')); } @@ -572,6 +579,20 @@ private function facebookHttp(): PendingRequest return $this->socialHttp()->asForm(); } + + /** + * @param array $payload + * @return array + */ + private function withPlace(array $payload): array + { + if ($this->placeId !== null) { + $payload['place'] = $this->placeId; + } + + return $payload; + } + /** * POST a form payload to a Graph edge and turn any failure into the typed * exception, logging the redacted body under `$label` first. diff --git a/app/Services/Social/FirstCommentPoster.php b/app/Services/Social/FirstCommentPoster.php new file mode 100644 index 000000000..39f9a3771 --- /dev/null +++ b/app/Services/Social/FirstCommentPoster.php @@ -0,0 +1,102 @@ +meta, 'first_comment')); + + if ($comment === '') { + return; + } + + try { + match ($post->platform) { + Platform::YouTube => $this->postYouTubeComment($post, $externalId, $comment), + Platform::Instagram, Platform::InstagramFacebook => $this->postInstagramComment($post, $externalId, $comment), + default => Log::warning('First comment is not supported for this platform', [ + 'platform' => $post->platform->value, + 'post_id' => $post->id, + ]), + }; + } catch (\Throwable $e) { + Log::warning('First comment failed (post itself is published)', [ + 'platform' => $post->platform->value, + 'post_id' => $post->id, + 'error' => $e->getMessage(), + ]); + } + } + + /** + * commentThreads.insert needs the youtube.force-ssl scope — requested at + * connect time since the platform was added, so every account has it. + */ + private function postYouTubeComment(Post $post, string $videoId, string $comment): void + { + $account = $post->socialAccount; + $api = rtrim((string) config('trypost.platforms.youtube.data_api'), '/'); + + $response = $this->socialHttp() + ->withToken($account->access_token) + ->post("{$api}/commentThreads?part=snippet", [ + 'snippet' => [ + 'videoId' => $videoId, + 'topLevelComment' => [ + 'snippet' => ['textOriginal' => $comment], + ], + ], + ]); + + if ($response->failed()) { + Log::warning('YouTube first comment failed', [ + 'post_id' => $post->id, + 'status' => $response->status(), + 'body' => $this->redactResponseBody($response->body()), + ]); + } + } + + /** + * Needs instagram_business_manage_comments (direct) / instagram_manage_comments + * (via Facebook Page) — accounts connected before the scope was requested + * get a 4xx here, which is logged and ignored; reconnecting fixes it. + */ + private function postInstagramComment(Post $post, string $mediaId, string $comment): void + { + $account = $post->socialAccount; + $baseUrl = $account->platform->instagramGraphBaseUrl(); + + $response = $this->socialHttp()->post("{$baseUrl}/{$mediaId}/comments", [ + 'message' => $comment, + 'access_token' => $account->access_token, + ]); + + if ($response->failed()) { + Log::warning('Instagram first comment failed', [ + 'post_id' => $post->id, + 'status' => $response->status(), + 'body' => $this->redactResponseBody($response->body()), + ]); + } + } +} diff --git a/app/Services/Social/InstagramPublisher.php b/app/Services/Social/InstagramPublisher.php index 319ada9e1..c44a52fc9 100644 --- a/app/Services/Social/InstagramPublisher.php +++ b/app/Services/Social/InstagramPublisher.php @@ -40,6 +40,13 @@ class InstagramPublisher private const string WORKFLOW_FINAL_CONTAINER = 'final_container'; + /** + * Facebook place ID from the post's `meta.location_id` — tags the post + * with a location. Applied to feed/reel/carousel containers; the API + * does not accept it on stories or carousel children. + */ + private ?string $locationId = null; + public function publish(Post $post): array { $this->post = $post; @@ -47,6 +54,7 @@ public function publish(Post $post): array $account = $post->socialAccount; $this->baseUrl = $account->platform->instagramGraphBaseUrl(); + $this->locationId = trim((string) data_get($post->meta, 'location_id')) ?: null; if ($account->needsProactiveTokenRefresh()) { app(ConnectionVerifier::class)->refreshToken($account); @@ -101,6 +109,19 @@ private function publishFeed(string $instagramId, string $accessToken, ?string $ return $this->publishSingleImage($instagramId, $accessToken, $content, $firstMedia); } + /** + * @param array $params + * @return array + */ + private function withLocation(array $params): array + { + if ($this->locationId !== null) { + $params['location_id'] = $this->locationId; + } + + return $params; + } + private function publishSingleImage(string $instagramId, string $accessToken, ?string $content, $media): array { $params = [ @@ -117,14 +138,14 @@ private function publishSingleImage(string $instagramId, string $accessToken, ?s $params = [...$params, ...$this->userTagsParam($media->userTags()), ...$this->sharedOptions()]; - $containerId = $this->createContainer($instagramId, $params, 'container'); + $containerId = $this->createContainer($instagramId, $this->withLocation($params), 'container'); return $this->finishContainer($instagramId, $accessToken, $containerId); } private function publishReel(string $instagramId, string $accessToken, ?string $content, $media): array { - $containerId = $this->createContainer($instagramId, [ + $containerId = $this->createContainer($instagramId, $this->withLocation([ 'video_url' => $media->url, 'caption' => $content, 'media_type' => 'REELS', @@ -132,7 +153,7 @@ private function publishReel(string $instagramId, string $accessToken, ?string $ ...$this->thumbOffsetParam($media), ...$this->sharedOptions(), ...$this->reelOptions(), - ], 'reel container'); + ]), 'reel container'); return $this->finishContainer($instagramId, $accessToken, $containerId); } @@ -296,13 +317,13 @@ private function finishCarousel(string $instagramId, string $accessToken, ?strin $this->waitForMediaProcessing($childId, $accessToken, $workflow); } - $carouselId = $this->createContainer($instagramId, [ + $carouselId = $this->createContainer($instagramId, $this->withLocation([ 'media_type' => 'CAROUSEL', 'caption' => $content, 'children' => implode(',', $childContainers), 'access_token' => $accessToken, ...$this->sharedOptions(), - ], 'carousel container'); + ]), 'carousel container'); return $this->finishContainer($instagramId, $accessToken, $carouselId); } diff --git a/app/Services/Social/MastodonPublisher.php b/app/Services/Social/MastodonPublisher.php index 85497a0f7..9a5695e96 100644 --- a/app/Services/Social/MastodonPublisher.php +++ b/app/Services/Social/MastodonPublisher.php @@ -82,7 +82,7 @@ private function mediaPayload(SocialAccount $account, string $instance, Collecti $mediaIds = []; foreach ($media->take(4) as $item) { - $mediaIds[] = $this->uploadMedia($account, $instance, $item->url, $item->original_filename, $item->isImage() ? $item->altTextFor(Platform::Mastodon) : null); + $mediaIds[] = $this->uploadMedia($account, $instance, $item->url, $item->original_filename, $item->altTextFor(Platform::Mastodon)); } return $mediaIds === [] ? [] : ['media_ids' => $mediaIds]; diff --git a/app/Services/Social/YouTubePublisher.php b/app/Services/Social/YouTubePublisher.php index 0f98c6c5d..c1d315bca 100644 --- a/app/Services/Social/YouTubePublisher.php +++ b/app/Services/Social/YouTubePublisher.php @@ -18,7 +18,9 @@ use Google\Client as GoogleClient; use Google\Service\Exception; use Google\Service\YouTube; +use Google\Service\YouTube\GeoPoint; use Google\Service\YouTube\Video; +use Google\Service\YouTube\VideoRecordingDetails; use Google\Service\YouTube\VideoSnippet; use Google\Service\YouTube\VideoStatus; use Google_Http_MediaFileUpload; @@ -190,7 +192,7 @@ private function uploadVideo(MediaItem $media, SocialAccount $account, string $p $youtube = new YouTube($client); $mediaUpload = new Google_Http_MediaFileUpload( $client, - $youtube->videos->insert('snippet,status', $video, $insertParameters), + $youtube->videos->insert($video->getRecordingDetails() !== null ? 'snippet,status,recordingDetails' : 'snippet,status', $video, $insertParameters), $media->mime_type ?: 'video/mp4', null, true, @@ -281,10 +283,37 @@ private function buildVideo(array $meta, string $content, string $description): $status->setSelfDeclaredMadeForKids((bool) data_get($meta, 'made_for_kids', false)); $status->setContainsSyntheticMedia((bool) data_get($meta, 'is_ai_generated', false)); + $tags = array_values(array_filter(array_map( + fn ($tag) => trim((string) $tag), + (array) data_get($meta, 'tags', []), + ))); + if ($tags !== []) { + $snippet->setTags($tags); + } + + if (filled($language = data_get($meta, 'default_language'))) { + $snippet->setDefaultLanguage((string) $language); + } + $video = new Video; $video->setSnippet($snippet); $video->setStatus($status); + $recording = data_get($meta, 'recording_location'); + if (is_array($recording) && isset($recording['lat'], $recording['lng'])) { + $location = new GeoPoint; + $location->setLatitude((float) $recording['lat']); + $location->setLongitude((float) $recording['lng']); + + $details = new VideoRecordingDetails; + $details->setLocation($location); + if (filled($recording['description'] ?? null)) { + $details->setLocationDescription((string) $recording['description']); + } + + $video->setRecordingDetails($details); + } + return $video; } } diff --git a/app/Support/PostPlatformMetaRules.php b/app/Support/PostPlatformMetaRules.php index cff025377..c12ebed67 100644 --- a/app/Support/PostPlatformMetaRules.php +++ b/app/Support/PostPlatformMetaRules.php @@ -80,6 +80,26 @@ public static function rules(string $prefix = 'meta'): array "{$prefix}.brand_content_toggle" => ['sometimes', 'boolean'], "{$prefix}.brand_organic_toggle" => ['sometimes', 'boolean'], + // First comment posted right after a successful publish (YouTube and + // Instagram). Capped at Instagram's comment limit, the stricter of + // the two. + "{$prefix}.first_comment" => ['sometimes', 'nullable', 'string', 'max:2200'], + + // Instagram / Facebook — place tag. Meta closed place search to + // third parties, so the ID is entered by hand; location_name is + // only for display in the editor. + "{$prefix}.location_id" => ['sometimes', 'nullable', 'string', 'max:64'], + "{$prefix}.location_name" => ['sometimes', 'nullable', 'string', 'max:200'], + + // YouTube (title and category_id reuse the shared rules below) + "{$prefix}.tags" => ['sometimes', 'nullable', 'array', 'max:30'], + "{$prefix}.tags.*" => ['required', 'string', 'max:100'], + "{$prefix}.default_language" => ['sometimes', 'nullable', 'string', 'max:12'], + "{$prefix}.recording_location" => ['sometimes', 'nullable', 'array'], + "{$prefix}.recording_location.lat" => ["required_with:{$prefix}.recording_location", 'numeric', 'between:-90,90'], + "{$prefix}.recording_location.lng" => ["required_with:{$prefix}.recording_location", 'numeric', 'between:-180,180'], + "{$prefix}.recording_location.description" => ['sometimes', 'nullable', 'string', 'max:200'], + "{$prefix}.board_id" => ['sometimes', 'nullable', 'string'], "{$prefix}.title" => ['sometimes', 'nullable', 'string', 'max:100'], "{$prefix}.link" => ['sometimes', 'nullable', 'url:http,https', 'max:2048'], @@ -177,6 +197,7 @@ public static function messages(string $prefix = 'meta'): array "{$prefix}.title.max" => __('posts.form.pinterest.title_max'), "{$prefix}.event.end_date.after_or_equal" => __('posts.form.google_business.event_end_date_before_start'), "{$prefix}.event.title.max" => __('posts.form.google_business.title_max'), + "{$prefix}.first_comment.max" => __('posts.form.first_comment.max'), ]; } @@ -193,6 +214,7 @@ public static function attributes(string $prefix = 'meta'): array "{$prefix}.link" => __('posts.form.pinterest.link'), "{$prefix}.event.title" => __('posts.form.google_business.event_title'), "{$prefix}.call_to_action.url" => __('posts.form.google_business.cta_url'), + "{$prefix}.first_comment" => __('posts.form.first_comment.label'), ]; } @@ -215,6 +237,8 @@ public static function documentation(): string 'Facebook, Bluesky, LinkedIn: link_preview (default true; false publishes a text post with a link without its preview card).', 'LinkedIn (profile and page): document_title (≤300, the title shown on a PDF document post; defaults to the file name).', 'Google Business Profile: topic_type STANDARD (default)|EVENT|OFFER; call_to_action {action_type: '.implode('|', array_column(CtaAction::cases(), 'value')).', url} (not on OFFER; url required unless NONE or CALL); event {title ≤'.TopicType::TITLE_MAX_LENGTH.', start_date, end_date (YYYY-MM-DD), start_time, end_time (HH:MM)} (required on EVENT and OFFER, the title being the offer title); offer {coupon_code, redeem_online_url, terms_conditions ≤5000} (OFFER only).', + 'YouTube Shorts and Instagram: first_comment (≤2200, posted by the account right after a successful publish).', + 'Instagram/Facebook: location_id (Facebook place ID) + location_name (display label). YouTube Shorts also: tags (string array, ≤30 of ≤100 chars), default_language (BCP-47), recording_location {lat, lng, description}.', 'Telegram, Facebook reels and stories: no settings. Stories (Instagram and Facebook) publish no text.', 'Threads of posts: thread_replies (list of up to '.ThreadReplies::MAX_REPLIES.' replies published under the post as a thread, each {text, media} where media is a list of up to 4 media items ({url}, {id} or {upload_token}, with optional meta.alt_text) for that reply alone; a plain string is a text-only reply) on Bluesky, Mastodon and X only; each reply needs text or media, its media follows the rules of a post on that network, and its text must fit the account limit (Bluesky 300, Mastodon 500 including the content warning, which every reply repeats, X 280 or 25000 for accounts with long posts).', ]); diff --git a/lang/ar/posts.php b/lang/ar/posts.php index 359071350..df67ede49 100644 --- a/lang/ar/posts.php +++ b/lang/ar/posts.php @@ -394,6 +394,34 @@ 'sports' => 'الرياضة', 'travel_and_events' => 'السفر والفعاليات', ], + 'tags' => 'الوسوم', + 'tags_placeholder' => 'brisbane, pixel art, map', + 'tags_hint' => 'مفصولة بفواصل؛ تساعد البحث والتوصيات.', + 'language' => 'اللغة', + 'location' => 'مكان التصوير', + 'location_placeholder' => 'Brisbane QLD', + 'lat' => 'خط العرض', + 'lng' => 'خط الطول', + ], + 'location' => [ + 'label' => 'الموقع', + 'id_placeholder' => 'معرّف المكان', + 'name_placeholder' => 'الاسم (للعرض)', + 'hint' => 'معرّف مكان فيسبوك؛ البحث عن الأماكن مغلق للتطبيقات الخارجية — أدخل المعرّف مرة واحدة وأعد استخدامه.', + ], + 'first_comment' => [ + 'label' => 'التعليق الأول', + 'placeholder' => 'رابط وإضافات للتعليق الأول…', + 'hint' => 'يُنشر باسم قناتك فور نشر الفيديو.', + 'hint_instagram' => 'يُنشر فور النشر — المكان الكلاسيكي للروابط التي لا تكون قابلة للنقر في وصف Instagram. أعد ربط الحساب مرة واحدة لمنح إذن التعليقات.', + 'max' => 'لا يمكن أن يتجاوز التعليق الأول 2200 حرف.', + ], + 'first_comment' => [ + 'label' => 'التعليق الأول', + 'placeholder' => 'رابط وإضافات للتعليق الأول…', + 'hint' => 'يُنشر باسم قناتك فور نشر الفيديو.', + 'hint_instagram' => 'يُنشر فور النشر — المكان الكلاسيكي للروابط التي لا تكون قابلة للنقر في وصف Instagram. أعد ربط الحساب مرة واحدة لمنح إذن التعليقات.', + 'max' => 'لا يمكن أن يتجاوز التعليق الأول 2200 حرف.', ], 'ai_generated' => [ 'label' => 'بالذكاء الاصطناعي', diff --git a/lang/de/posts.php b/lang/de/posts.php index 34fdd6f91..371bc2206 100644 --- a/lang/de/posts.php +++ b/lang/de/posts.php @@ -396,6 +396,34 @@ 'sports' => 'Sport', 'travel_and_events' => 'Reisen & Events', ], + 'tags' => 'Tags', + 'tags_placeholder' => 'brisbane, pixel art, stadtkarte', + 'tags_hint' => 'Kommagetrennt; hilft Suche und Empfehlungen.', + 'language' => 'Sprache', + 'location' => 'Aufnahmeort', + 'location_placeholder' => 'Brisbane QLD', + 'lat' => 'Breitengrad', + 'lng' => 'Längengrad', + ], + 'location' => [ + 'label' => 'Standort', + 'id_placeholder' => 'Orts-ID', + 'name_placeholder' => 'Name (zur Anzeige)', + 'hint' => 'Facebook-Orts-ID; die Ortssuche ist für Dritt-Apps geschlossen — ID einmal ermitteln und wiederverwenden.', + ], + 'first_comment' => [ + 'label' => 'Erster Kommentar', + 'placeholder' => 'Link und Extras für den ersten Kommentar…', + 'hint' => 'Wird direkt nach der Veröffentlichung vom Kanal gepostet.', + 'hint_instagram' => 'Wird direkt nach dem Beitrag gepostet — der klassische Ort für Links, die in Instagram-Beschreibungen nicht klickbar sind. Konto einmal neu verbinden, um die Kommentar-Berechtigung zu erteilen.', + 'max' => 'Der erste Kommentar darf 2200 Zeichen nicht überschreiten.', + ], + 'first_comment' => [ + 'label' => 'Erster Kommentar', + 'placeholder' => 'Link und Extras für den ersten Kommentar…', + 'hint' => 'Wird direkt nach der Veröffentlichung vom Kanal gepostet.', + 'hint_instagram' => 'Wird direkt nach dem Beitrag gepostet — der klassische Ort für Links, die in Instagram-Beschreibungen nicht klickbar sind. Konto einmal neu verbinden, um die Kommentar-Berechtigung zu erteilen.', + 'max' => 'Der erste Kommentar darf 2200 Zeichen nicht überschreiten.', ], 'ai_generated' => [ 'label' => 'KI-generiert', diff --git a/lang/el/posts.php b/lang/el/posts.php index 40a5fd543..02ff1a465 100644 --- a/lang/el/posts.php +++ b/lang/el/posts.php @@ -394,6 +394,34 @@ 'sports' => 'Αθλητικά', 'travel_and_events' => 'Ταξίδια και εκδηλώσεις', ], + 'tags' => 'Ετικέτες', + 'tags_placeholder' => 'brisbane, pixel art, map', + 'tags_hint' => 'Χωρισμένες με κόμματα· βοηθούν αναζήτηση και προτάσεις.', + 'language' => 'Γλώσσα', + 'location' => 'Τοποθεσία λήψης', + 'location_placeholder' => 'Brisbane QLD', + 'lat' => 'Γεωγρ. πλάτος', + 'lng' => 'Γεωγρ. μήκος', + ], + 'location' => [ + 'label' => 'Τοποθεσία', + 'id_placeholder' => 'ID τοποθεσίας', + 'name_placeholder' => 'Όνομα (εμφάνιση)', + 'hint' => 'Facebook place ID· η αναζήτηση τοποθεσιών είναι κλειστή για τρίτους — βρείτε το ID μία φορά και επαναχρησιμοποιήστε το.', + ], + 'first_comment' => [ + 'label' => 'Πρώτο σχόλιο', + 'placeholder' => 'Σύνδεσμος και πρόσθετα για το πρώτο σχόλιο…', + 'hint' => 'Δημοσιεύεται από το κανάλι σας αμέσως μετά τη δημοσίευση του βίντεο.', + 'hint_instagram' => 'Δημοσιεύεται αμέσως μετά την ανάρτηση — το κλασικό σημείο για συνδέσμους που οι λεζάντες του Instagram δεν κάνουν κλικαρίσιμους. Επανασυνδέστε τον λογαριασμό μία φορά για να δώσετε το δικαίωμα σχολίων.', + 'max' => 'Το πρώτο σχόλιο δεν μπορεί να υπερβαίνει τους 2200 χαρακτήρες.', + ], + 'first_comment' => [ + 'label' => 'Πρώτο σχόλιο', + 'placeholder' => 'Σύνδεσμος και πρόσθετα για το πρώτο σχόλιο…', + 'hint' => 'Δημοσιεύεται από το κανάλι σας αμέσως μετά τη δημοσίευση του βίντεο.', + 'hint_instagram' => 'Δημοσιεύεται αμέσως μετά την ανάρτηση — το κλασικό σημείο για συνδέσμους που οι λεζάντες του Instagram δεν κάνουν κλικαρίσιμους. Επανασυνδέστε τον λογαριασμό μία φορά για να δώσετε το δικαίωμα σχολίων.', + 'max' => 'Το πρώτο σχόλιο δεν μπορεί να υπερβαίνει τους 2200 χαρακτήρες.', ], 'ai_generated' => [ 'label' => 'Με AI', diff --git a/lang/en/posts.php b/lang/en/posts.php index d469dee19..8ed777af9 100644 --- a/lang/en/posts.php +++ b/lang/en/posts.php @@ -394,6 +394,34 @@ 'sports' => 'Sports', 'travel_and_events' => 'Travel & Events', ], + 'tags' => 'Tags', + 'tags_placeholder' => 'brisbane, pixel art, city map', + 'tags_hint' => 'Comma-separated; helps search and recommendations.', + 'language' => 'Language', + 'location' => 'Recording location', + 'location_placeholder' => 'Brisbane QLD', + 'lat' => 'Latitude', + 'lng' => 'Longitude', + ], + 'location' => [ + 'label' => 'Location', + 'id_placeholder' => 'Place ID', + 'name_placeholder' => 'Name (for display)', + 'hint' => 'Facebook place ID; place search is closed to third-party apps, so enter the ID once and reuse it.', + ], + 'first_comment' => [ + 'label' => 'First comment', + 'placeholder' => 'Link and extras for the first comment…', + 'hint' => 'Posted by your channel right after the video is published.', + 'hint_instagram' => 'Posted right after publishing — the classic spot for links Instagram captions can\'t make clickable. Requires reconnecting the account once to grant the comments permission.', + 'max' => 'First comment may not exceed 2200 characters.', + ], + 'first_comment' => [ + 'label' => 'First comment', + 'placeholder' => 'Link and extras for the first comment…', + 'hint' => 'Posted by your channel right after the video is published.', + 'hint_instagram' => 'Posted right after publishing — the classic spot for links Instagram captions can\'t make clickable. Requires reconnecting the account once to grant the comments permission.', + 'max' => 'First comment may not exceed 2200 characters.', ], 'ai_generated' => [ 'label' => 'AI-generated', diff --git a/lang/es/posts.php b/lang/es/posts.php index a699ac95f..53bcd44e9 100644 --- a/lang/es/posts.php +++ b/lang/es/posts.php @@ -394,6 +394,34 @@ 'sports' => 'Deportes', 'travel_and_events' => 'Viajes y eventos', ], + 'tags' => 'Etiquetas', + 'tags_placeholder' => 'brisbane, pixel art, mapa', + 'tags_hint' => 'Separadas por comas; ayudan a la búsqueda y recomendaciones.', + 'language' => 'Idioma', + 'location' => 'Lugar de grabación', + 'location_placeholder' => 'Brisbane QLD', + 'lat' => 'Latitud', + 'lng' => 'Longitud', + ], + 'location' => [ + 'label' => 'Ubicación', + 'id_placeholder' => 'ID del lugar', + 'name_placeholder' => 'Nombre (visual)', + 'hint' => 'ID de lugar de Facebook; la búsqueda de lugares está cerrada a terceros: introduce el ID una vez y reutilízalo.', + ], + 'first_comment' => [ + 'label' => 'Primer comentario', + 'placeholder' => 'Enlace y extras para el primer comentario…', + 'hint' => 'Se publica desde tu canal justo después de publicar el vídeo.', + 'hint_instagram' => 'Se publica justo después de la publicación: el lugar clásico para enlaces que Instagram no hace clicables en la descripción. Reconecta la cuenta una vez para conceder el permiso de comentarios.', + 'max' => 'El primer comentario no puede superar los 2200 caracteres.', + ], + 'first_comment' => [ + 'label' => 'Primer comentario', + 'placeholder' => 'Enlace y extras para el primer comentario…', + 'hint' => 'Se publica desde tu canal justo después de publicar el vídeo.', + 'hint_instagram' => 'Se publica justo después de la publicación: el lugar clásico para enlaces que Instagram no hace clicables en la descripción. Reconecta la cuenta una vez para conceder el permiso de comentarios.', + 'max' => 'El primer comentario no puede superar los 2200 caracteres.', ], 'ai_generated' => [ 'label' => 'Generado con IA', diff --git a/lang/fr/posts.php b/lang/fr/posts.php index 721c6040c..227bd1b96 100644 --- a/lang/fr/posts.php +++ b/lang/fr/posts.php @@ -394,6 +394,34 @@ 'sports' => 'Sport', 'travel_and_events' => 'Voyages et événements', ], + 'tags' => 'Tags', + 'tags_placeholder' => 'brisbane, pixel art, carte', + 'tags_hint' => 'Séparés par des virgules ; aide la recherche et les recommandations.', + 'language' => 'Langue', + 'location' => 'Lieu de tournage', + 'location_placeholder' => 'Brisbane QLD', + 'lat' => 'Latitude', + 'lng' => 'Longitude', + ], + 'location' => [ + 'label' => 'Lieu', + 'id_placeholder' => 'ID du lieu', + 'name_placeholder' => 'Nom (affichage)', + 'hint' => 'ID de lieu Facebook ; la recherche de lieux est fermée aux apps tierces — saisissez l’ID une fois et réutilisez-le.', + ], + 'first_comment' => [ + 'label' => 'Premier commentaire', + 'placeholder' => 'Lien et compléments pour le premier commentaire…', + 'hint' => 'Publié par votre chaîne juste après la mise en ligne de la vidéo.', + 'hint_instagram' => 'Publié juste après la publication — l\'endroit classique pour les liens que les légendes Instagram ne rendent pas cliquables. Reconnectez le compte une fois pour accorder l\'autorisation de commenter.', + 'max' => 'Le premier commentaire ne peut pas dépasser 2200 caractères.', + ], + 'first_comment' => [ + 'label' => 'Premier commentaire', + 'placeholder' => 'Lien et compléments pour le premier commentaire…', + 'hint' => 'Publié par votre chaîne juste après la mise en ligne de la vidéo.', + 'hint_instagram' => 'Publié juste après la publication — l\'endroit classique pour les liens que les légendes Instagram ne rendent pas cliquables. Reconnectez le compte une fois pour accorder l\'autorisation de commenter.', + 'max' => 'Le premier commentaire ne peut pas dépasser 2200 caractères.', ], 'ai_generated' => [ 'label' => 'Généré par IA', diff --git a/lang/it/posts.php b/lang/it/posts.php index 4a48423f9..5825cbf10 100644 --- a/lang/it/posts.php +++ b/lang/it/posts.php @@ -394,6 +394,34 @@ 'sports' => 'Sport', 'travel_and_events' => 'Viaggi ed eventi', ], + 'tags' => 'Tag', + 'tags_placeholder' => 'brisbane, pixel art, mappa', + 'tags_hint' => 'Separati da virgole; aiutano ricerca e consigli.', + 'language' => 'Lingua', + 'location' => 'Luogo di ripresa', + 'location_placeholder' => 'Brisbane QLD', + 'lat' => 'Latitudine', + 'lng' => 'Longitudine', + ], + 'location' => [ + 'label' => 'Posizione', + 'id_placeholder' => 'ID del luogo', + 'name_placeholder' => 'Nome (visuale)', + 'hint' => 'ID luogo di Facebook; la ricerca dei luoghi è chiusa alle app di terzi: inserisci l’ID una volta e riusalo.', + ], + 'first_comment' => [ + 'label' => 'Primo commento', + 'placeholder' => 'Link ed extra per il primo commento…', + 'hint' => 'Pubblicato dal tuo canale subito dopo la pubblicazione del video.', + 'hint_instagram' => 'Pubblicato subito dopo il post: il posto classico per i link che le didascalie di Instagram non rendono cliccabili. Ricollega l\'account una volta per concedere il permesso sui commenti.', + 'max' => 'Il primo commento non può superare i 2200 caratteri.', + ], + 'first_comment' => [ + 'label' => 'Primo commento', + 'placeholder' => 'Link ed extra per il primo commento…', + 'hint' => 'Pubblicato dal tuo canale subito dopo la pubblicazione del video.', + 'hint_instagram' => 'Pubblicato subito dopo il post: il posto classico per i link che le didascalie di Instagram non rendono cliccabili. Ricollega l\'account una volta per concedere il permesso sui commenti.', + 'max' => 'Il primo commento non può superare i 2200 caratteri.', ], 'ai_generated' => [ 'label' => 'Generato con IA', diff --git a/lang/ja/posts.php b/lang/ja/posts.php index 5b76c7947..d0826adca 100644 --- a/lang/ja/posts.php +++ b/lang/ja/posts.php @@ -394,6 +394,34 @@ 'sports' => 'スポーツ', 'travel_and_events' => '旅行とイベント', ], + 'tags' => 'タグ', + 'tags_placeholder' => 'brisbane, pixel art, map', + 'tags_hint' => 'カンマ区切り。検索とおすすめに役立ちます。', + 'language' => '言語', + 'location' => '撮影場所', + 'location_placeholder' => 'Brisbane QLD', + 'lat' => '緯度', + 'lng' => '経度', + ], + 'location' => [ + 'label' => '位置情報', + 'id_placeholder' => 'プレイスID', + 'name_placeholder' => '名称(表示用)', + 'hint' => 'FacebookのプレイスID。場所検索はサードパーティに閉鎖されているため、IDを一度調べて再利用してください。', + ], + 'first_comment' => [ + 'label' => '最初のコメント', + 'placeholder' => '最初のコメントに入れるリンクや補足…', + 'hint' => '動画公開直後にチャンネルから投稿されます。', + 'hint_instagram' => '投稿直後にコメントされます。Instagramのキャプションでクリックできないリンクの定番の置き場所です。コメント権限を付与するには一度アカウントを再連携してください。', + 'max' => '最初のコメントは2200文字を超えられません。', + ], + 'first_comment' => [ + 'label' => '最初のコメント', + 'placeholder' => '最初のコメントに入れるリンクや補足…', + 'hint' => '動画公開直後にチャンネルから投稿されます。', + 'hint_instagram' => '投稿直後にコメントされます。Instagramのキャプションでクリックできないリンクの定番の置き場所です。コメント権限を付与するには一度アカウントを再連携してください。', + 'max' => '最初のコメントは2200文字を超えられません。', ], 'ai_generated' => [ 'label' => 'AI 生成', diff --git a/lang/ko/posts.php b/lang/ko/posts.php index bc3b61238..937234b8e 100644 --- a/lang/ko/posts.php +++ b/lang/ko/posts.php @@ -394,6 +394,34 @@ 'sports' => '스포츠', 'travel_and_events' => '여행/이벤트', ], + 'tags' => '태그', + 'tags_placeholder' => 'brisbane, pixel art, map', + 'tags_hint' => '쉼표로 구분. 검색과 추천에 도움이 됩니다.', + 'language' => '언어', + 'location' => '촬영 위치', + 'location_placeholder' => 'Brisbane QLD', + 'lat' => '위도', + 'lng' => '경도', + ], + 'location' => [ + 'label' => '위치', + 'id_placeholder' => '장소 ID', + 'name_placeholder' => '이름(표시용)', + 'hint' => 'Facebook 장소 ID. 장소 검색은 서드파티에 닫혀 있으므로 ID를 한 번 찾아 재사용하세요.', + ], + 'first_comment' => [ + 'label' => '첫 댓글', + 'placeholder' => '첫 댓글에 넣을 링크와 추가 내용…', + 'hint' => '동영상 게시 직후 채널 명의로 작성됩니다.', + 'hint_instagram' => '게시 직후 작성됩니다 — Instagram 캡션에서 클릭되지 않는 링크를 넣는 고전적인 방법입니다. 댓글 권한을 부여하려면 계정을 한 번 다시 연결하세요.', + 'max' => '첫 댓글은 2200자를 초과할 수 없습니다.', + ], + 'first_comment' => [ + 'label' => '첫 댓글', + 'placeholder' => '첫 댓글에 넣을 링크와 추가 내용…', + 'hint' => '동영상 게시 직후 채널 명의로 작성됩니다.', + 'hint_instagram' => '게시 직후 작성됩니다 — Instagram 캡션에서 클릭되지 않는 링크를 넣는 고전적인 방법입니다. 댓글 권한을 부여하려면 계정을 한 번 다시 연결하세요.', + 'max' => '첫 댓글은 2200자를 초과할 수 없습니다.', ], 'ai_generated' => [ 'label' => 'AI 생성', diff --git a/lang/nl/posts.php b/lang/nl/posts.php index 6616a1edd..ac9cd7252 100644 --- a/lang/nl/posts.php +++ b/lang/nl/posts.php @@ -394,6 +394,34 @@ 'sports' => 'Sport', 'travel_and_events' => 'Reizen en evenementen', ], + 'tags' => 'Tags', + 'tags_placeholder' => 'brisbane, pixel art, kaart', + 'tags_hint' => 'Kommagescheiden; helpt zoeken en aanbevelingen.', + 'language' => 'Taal', + 'location' => 'Opnamelocatie', + 'location_placeholder' => 'Brisbane QLD', + 'lat' => 'Breedtegraad', + 'lng' => 'Lengtegraad', + ], + 'location' => [ + 'label' => 'Locatie', + 'id_placeholder' => 'Plaats-ID', + 'name_placeholder' => 'Naam (weergave)', + 'hint' => 'Facebook-plaats-ID; plaats zoeken is gesloten voor externe apps — voer de ID één keer in en hergebruik.', + ], + 'first_comment' => [ + 'label' => 'Eerste reactie', + 'placeholder' => 'Link en extra\'s voor de eerste reactie…', + 'hint' => 'Direct na publicatie door je kanaal geplaatst.', + 'hint_instagram' => 'Direct na het bericht geplaatst — dé plek voor links die Instagram-bijschriften niet klikbaar maken. Verbind het account één keer opnieuw om de reactietoestemming te geven.', + 'max' => 'De eerste reactie mag niet langer zijn dan 2200 tekens.', + ], + 'first_comment' => [ + 'label' => 'Eerste reactie', + 'placeholder' => 'Link en extra\'s voor de eerste reactie…', + 'hint' => 'Direct na publicatie door je kanaal geplaatst.', + 'hint_instagram' => 'Direct na het bericht geplaatst — dé plek voor links die Instagram-bijschriften niet klikbaar maken. Verbind het account één keer opnieuw om de reactietoestemming te geven.', + 'max' => 'De eerste reactie mag niet langer zijn dan 2200 tekens.', ], 'ai_generated' => [ 'label' => 'AI-gegenereerd', diff --git a/lang/pl/posts.php b/lang/pl/posts.php index 24374ca05..18006e36e 100644 --- a/lang/pl/posts.php +++ b/lang/pl/posts.php @@ -394,6 +394,34 @@ 'sports' => 'Sport', 'travel_and_events' => 'Podróże i wydarzenia', ], + 'tags' => 'Tagi', + 'tags_placeholder' => 'brisbane, pixel art, mapa', + 'tags_hint' => 'Rozdzielone przecinkami; pomagają wyszukiwarce i rekomendacjom.', + 'language' => 'Język', + 'location' => 'Miejsce nagrania', + 'location_placeholder' => 'Brisbane QLD', + 'lat' => 'Szerokość', + 'lng' => 'Długość', + ], + 'location' => [ + 'label' => 'Lokalizacja', + 'id_placeholder' => 'ID miejsca', + 'name_placeholder' => 'Nazwa (podgląd)', + 'hint' => 'ID miejsca Facebooka; wyszukiwanie miejsc jest zamknięte dla aplikacji zewnętrznych — wpisz ID raz i używaj ponownie.', + ], + 'first_comment' => [ + 'label' => 'Pierwszy komentarz', + 'placeholder' => 'Link i dodatki do pierwszego komentarza…', + 'hint' => 'Publikowany przez Twój kanał zaraz po opublikowaniu filmu.', + 'hint_instagram' => 'Publikowany zaraz po poście — klasyczne miejsce na linki, których opisy na Instagramie nie czynią klikalnymi. Połącz konto ponownie, aby nadać uprawnienie do komentarzy.', + 'max' => 'Pierwszy komentarz nie może przekraczać 2200 znaków.', + ], + 'first_comment' => [ + 'label' => 'Pierwszy komentarz', + 'placeholder' => 'Link i dodatki do pierwszego komentarza…', + 'hint' => 'Publikowany przez Twój kanał zaraz po opublikowaniu filmu.', + 'hint_instagram' => 'Publikowany zaraz po poście — klasyczne miejsce na linki, których opisy na Instagramie nie czynią klikalnymi. Połącz konto ponownie, aby nadać uprawnienie do komentarzy.', + 'max' => 'Pierwszy komentarz nie może przekraczać 2200 znaków.', ], 'ai_generated' => [ 'label' => 'Treść AI', diff --git a/lang/pt-BR/posts.php b/lang/pt-BR/posts.php index 7d6856cab..039c37a8d 100644 --- a/lang/pt-BR/posts.php +++ b/lang/pt-BR/posts.php @@ -394,6 +394,34 @@ 'sports' => 'Esportes', 'travel_and_events' => 'Viagens e eventos', ], + 'tags' => 'Tags', + 'tags_placeholder' => 'brisbane, pixel art, mapa', + 'tags_hint' => 'Separadas por vírgulas; ajudam busca e recomendações.', + 'language' => 'Idioma', + 'location' => 'Local de gravação', + 'location_placeholder' => 'Brisbane QLD', + 'lat' => 'Latitude', + 'lng' => 'Longitude', + ], + 'location' => [ + 'label' => 'Localização', + 'id_placeholder' => 'ID do lugar', + 'name_placeholder' => 'Nome (exibição)', + 'hint' => 'ID de lugar do Facebook; a busca de lugares está fechada para terceiros — informe o ID uma vez e reutilize.', + ], + 'first_comment' => [ + 'label' => 'Primeiro comentário', + 'placeholder' => 'Link e extras para o primeiro comentário…', + 'hint' => 'Publicado pelo seu canal logo após o vídeo ir ao ar.', + 'hint_instagram' => 'Publicado logo após a postagem — o lugar clássico para links que as legendas do Instagram não tornam clicáveis. Reconecte a conta uma vez para conceder a permissão de comentários.', + 'max' => 'O primeiro comentário não pode exceder 2200 caracteres.', + ], + 'first_comment' => [ + 'label' => 'Primeiro comentário', + 'placeholder' => 'Link e extras para o primeiro comentário…', + 'hint' => 'Publicado pelo seu canal logo após o vídeo ir ao ar.', + 'hint_instagram' => 'Publicado logo após a postagem — o lugar clássico para links que as legendas do Instagram não tornam clicáveis. Reconecte a conta uma vez para conceder a permissão de comentários.', + 'max' => 'O primeiro comentário não pode exceder 2200 caracteres.', ], 'ai_generated' => [ 'label' => 'Gerado por IA', diff --git a/lang/ru/posts.php b/lang/ru/posts.php index 7c0d0b927..3ec0b054b 100644 --- a/lang/ru/posts.php +++ b/lang/ru/posts.php @@ -394,6 +394,34 @@ 'sports' => 'Спорт', 'travel_and_events' => 'Путешествия', ], + 'tags' => 'Теги', + 'tags_placeholder' => 'брисбен, пиксель-арт, карта города', + 'tags_hint' => 'Через запятую; помогают поиску и рекомендациям.', + 'language' => 'Язык', + 'location' => 'Место съёмки', + 'location_placeholder' => 'Brisbane QLD', + 'lat' => 'Широта', + 'lng' => 'Долгота', + ], + 'location' => [ + 'label' => 'Локация', + 'id_placeholder' => 'ID места', + 'name_placeholder' => 'Название (для себя)', + 'hint' => 'ID места Facebook; поиск мест закрыт для сторонних приложений — найдите ID один раз и переиспользуйте.', + ], + 'first_comment' => [ + 'label' => 'Первый комментарий', + 'placeholder' => 'Ссылка и дополнения для первого комментария…', + 'hint' => 'Публикуется от имени канала сразу после выхода ролика.', + 'hint_instagram' => 'Публикуется сразу после поста — классическое место для ссылок, которые в описании Instagram некликабельны. Требуется один раз переподключить аккаунт, чтобы выдать право на комментарии.', + 'max' => 'Первый комментарий не может быть длиннее 2200 знаков.', + ], + 'first_comment' => [ + 'label' => 'Первый комментарий', + 'placeholder' => 'Ссылка и дополнения для первого комментария…', + 'hint' => 'Публикуется от имени канала сразу после выхода ролика.', + 'hint_instagram' => 'Публикуется сразу после поста — классическое место для ссылок, которые в описании Instagram некликабельны. Требуется один раз переподключить аккаунт, чтобы выдать право на комментарии.', + 'max' => 'Первый комментарий не может быть длиннее 2200 знаков.', ], 'ai_generated' => [ 'label' => 'Создано ИИ', diff --git a/lang/tr/posts.php b/lang/tr/posts.php index d45bd2a3a..df31e8f01 100644 --- a/lang/tr/posts.php +++ b/lang/tr/posts.php @@ -396,6 +396,34 @@ 'sports' => 'Spor', 'travel_and_events' => 'Seyahat ve Etkinlikler', ], + 'tags' => 'Etiketler', + 'tags_placeholder' => 'brisbane, pixel art, harita', + 'tags_hint' => 'Virgülle ayrılır; arama ve önerilere yardımcı olur.', + 'language' => 'Dil', + 'location' => 'Çekim yeri', + 'location_placeholder' => 'Brisbane QLD', + 'lat' => 'Enlem', + 'lng' => 'Boylam', + ], + 'location' => [ + 'label' => 'Konum', + 'id_placeholder' => 'Yer ID', + 'name_placeholder' => 'Ad (görünüm)', + 'hint' => 'Facebook yer ID’si; yer arama üçüncü taraflara kapalı — ID’yi bir kez bulun ve yeniden kullanın.', + ], + 'first_comment' => [ + 'label' => 'İlk yorum', + 'placeholder' => 'İlk yorum için bağlantı ve ekler…', + 'hint' => 'Video yayınlandıktan hemen sonra kanalınız tarafından gönderilir.', + 'hint_instagram' => 'Gönderiden hemen sonra paylaşılır — Instagram açıklamalarında tıklanamayan bağlantılar için klasik yer. Yorum iznini vermek için hesabı bir kez yeniden bağlayın.', + 'max' => 'İlk yorum 2200 karakteri aşamaz.', + ], + 'first_comment' => [ + 'label' => 'İlk yorum', + 'placeholder' => 'İlk yorum için bağlantı ve ekler…', + 'hint' => 'Video yayınlandıktan hemen sonra kanalınız tarafından gönderilir.', + 'hint_instagram' => 'Gönderiden hemen sonra paylaşılır — Instagram açıklamalarında tıklanamayan bağlantılar için klasik yer. Yorum iznini vermek için hesabı bir kez yeniden bağlayın.', + 'max' => 'İlk yorum 2200 karakteri aşamaz.', ], 'ai_generated' => [ 'label' => 'AI ile üretildi', diff --git a/lang/uk/posts.php b/lang/uk/posts.php index 4783f3423..d46953638 100644 --- a/lang/uk/posts.php +++ b/lang/uk/posts.php @@ -394,6 +394,34 @@ 'sports' => 'Спорт', 'travel_and_events' => 'Подорожі та події', ], + 'tags' => 'Теги', + 'tags_placeholder' => 'брисбен, піксель-арт, мапа міста', + 'tags_hint' => 'Через кому; допомагають пошуку та рекомендаціям.', + 'language' => 'Мова', + 'location' => 'Місце зйомки', + 'location_placeholder' => 'Brisbane QLD', + 'lat' => 'Широта', + 'lng' => 'Довгота', + ], + 'location' => [ + 'label' => 'Локація', + 'id_placeholder' => 'ID місця', + 'name_placeholder' => 'Назва (для себе)', + 'hint' => 'ID місця Facebook; пошук місць закритий для сторонніх застосунків — знайдіть ID один раз і повторно використовуйте.', + ], + 'first_comment' => [ + 'label' => 'Перший коментар', + 'placeholder' => 'Посилання та доповнення для першого коментаря…', + 'hint' => 'Публікується від імені каналу одразу після виходу відео.', + 'hint_instagram' => 'Публікується одразу після поста — класичне місце для посилань, які в описі Instagram неклікабельні. Потрібно один раз перепідключити акаунт, щоб надати право на коментарі.', + 'max' => 'Перший коментар не може перевищувати 2200 знаків.', + ], + 'first_comment' => [ + 'label' => 'Перший коментар', + 'placeholder' => 'Посилання та доповнення для першого коментаря…', + 'hint' => 'Публікується від імені каналу одразу після виходу відео.', + 'hint_instagram' => 'Публікується одразу після поста — класичне місце для посилань, які в описі Instagram неклікабельні. Потрібно один раз перепідключити акаунт, щоб надати право на коментарі.', + 'max' => 'Перший коментар не може перевищувати 2200 знаків.', ], 'ai_generated' => [ 'label' => 'Створено AI', diff --git a/lang/zh/posts.php b/lang/zh/posts.php index 4de091b98..9c17bba22 100644 --- a/lang/zh/posts.php +++ b/lang/zh/posts.php @@ -394,6 +394,34 @@ 'sports' => '体育', 'travel_and_events' => '旅游和活动', ], + 'tags' => '标签', + 'tags_placeholder' => 'brisbane, pixel art, map', + 'tags_hint' => '用逗号分隔;有助于搜索和推荐。', + 'language' => '语言', + 'location' => '拍摄地点', + 'location_placeholder' => 'Brisbane QLD', + 'lat' => '纬度', + 'lng' => '经度', + ], + 'location' => [ + 'label' => '位置', + 'id_placeholder' => '地点 ID', + 'name_placeholder' => '名称(展示用)', + 'hint' => 'Facebook 地点 ID;地点搜索已对第三方关闭,请查一次 ID 后重复使用。', + ], + 'first_comment' => [ + 'label' => '首条评论', + 'placeholder' => '首条评论中的链接和补充内容…', + 'hint' => '视频发布后立即以频道名义发表。', + 'hint_instagram' => '发布后立即评论——这是放置 Instagram 描述中无法点击的链接的经典位置。需重新连接一次账号以授予评论权限。', + 'max' => '首条评论不能超过 2200 个字符。', + ], + 'first_comment' => [ + 'label' => '首条评论', + 'placeholder' => '首条评论中的链接和补充内容…', + 'hint' => '视频发布后立即以频道名义发表。', + 'hint_instagram' => '发布后立即评论——这是放置 Instagram 描述中无法点击的链接的经典位置。需重新连接一次账号以授予评论权限。', + 'max' => '首条评论不能超过 2200 个字符。', ], 'ai_generated' => [ 'label' => 'AI 生成', diff --git a/resources/js/components/posts/composer/ComposerNetworkSettings.vue b/resources/js/components/posts/composer/ComposerNetworkSettings.vue index 078999d53..7a12eca46 100644 --- a/resources/js/components/posts/composer/ComposerNetworkSettings.vue +++ b/resources/js/components/posts/composer/ComposerNetworkSettings.vue @@ -4,6 +4,7 @@ import { computed } from 'vue'; import ComposerLiveDataPending from '@/components/posts/composer/ComposerLiveDataPending.vue'; import AiGeneratedRow from '@/components/posts/editor/AiGeneratedRow.vue'; import DiscordSettings from '@/components/posts/editor/DiscordSettings.vue'; +import FacebookSettings from '@/components/posts/editor/FacebookSettings.vue'; import GoogleBusinessSettings from '@/components/posts/editor/GoogleBusinessSettings.vue'; import InstagramSettings from '@/components/posts/editor/InstagramSettings.vue'; import LinkedInSettings from '@/components/posts/editor/LinkedInSettings.vue'; @@ -114,6 +115,12 @@ const aiGenerated = computed({ :disabled="disabled" @update:meta="update" /> + +import { computed } from 'vue'; + +import SettingsRow from '@/components/posts/editor/SettingsRow.vue'; +import SettingsSection from '@/components/posts/editor/SettingsSection.vue'; +import { Input } from '@/components/ui/input'; +import { toNullableText } from '@/lib/utils'; + +const props = withDefaults( + defineProps<{ + meta?: Record; + disabled?: boolean; + }>(), + { meta: () => ({}), disabled: false }, +); + +const emit = defineEmits<{ 'update:meta': [value: Record] }>(); + +const locationId = computed({ + get: () => toNullableText(props.meta.location_id) ?? '', + set: (value: string) => + emit('update:meta', { + ...props.meta, + location_id: toNullableText(value.trim()), + }), +}); +const locationName = computed({ + get: () => toNullableText(props.meta.location_name) ?? '', + set: (value: string) => + emit('update:meta', { + ...props.meta, + location_name: toNullableText(value), + }), +}); + + + diff --git a/resources/js/components/posts/editor/InstagramSettings.vue b/resources/js/components/posts/editor/InstagramSettings.vue index e92d5f015..f00697199 100644 --- a/resources/js/components/posts/editor/InstagramSettings.vue +++ b/resources/js/components/posts/editor/InstagramSettings.vue @@ -1,10 +1,14 @@