trypost/lang/en/posts.php

671 lines
30 KiB
PHP
Raw Permalink Normal View History

2026-01-21 23:58:08 +00:00
<?php
return [
'title' => 'Posts',
2026-03-31 00:18:07 +00:00
'search' => 'Search posts...',
2026-01-21 23:58:08 +00:00
'all_posts' => 'All Posts',
'new_post' => 'New Post',
'no_posts' => 'No posts found',
'no_search_results' => 'No posts match your search',
'try_different_search' => 'Try a different keyword or clear the search.',
2026-01-21 23:58:08 +00:00
'start_creating' => 'Start by creating your first post.',
'filter_by_label' => 'Filter by label',
'label_search_placeholder' => 'Search labels...',
'no_labels' => 'No labels found.',
'clear_label_filter' => 'Clear label filter',
'table' => [
'post' => 'Post',
'status' => 'Status',
'content' => 'Content',
'platforms' => 'Platforms',
'labels' => 'Labels',
'scheduled_at' => 'Date',
'actions' => '',
],
2026-01-21 23:58:08 +00:00
'manage_posts' => 'Manage all your posts',
'delete_confirm' => 'Are you sure you want to delete this post?',
'by' => 'by',
2026-01-22 00:33:31 +00:00
'actions' => [
'view' => 'View post',
'delete' => 'Delete',
'duplicate' => 'Duplicate',
'copy_id' => 'Copy ID',
'copied' => 'ID copied to clipboard',
2026-01-22 00:33:31 +00:00
],
2026-01-22 00:46:27 +00:00
'form' => [
'post_type' => 'Post Type',
2026-01-22 00:49:45 +00:00
'board' => 'Board',
'select_board' => 'Select a board',
'search_board' => 'Search board...',
'no_board_found' => 'No board found',
'media' => 'Media',
'min' => 'Min',
'uploading' => 'Uploading...',
'drop_to_upload' => 'Drop to upload',
'drag_and_drop' => 'Drag & drop or click to upload',
'photos_and_videos' => 'Photos and videos',
'photos_only' => 'Photos only',
'videos_only' => 'Videos only',
'drag_to_reorder' => 'Drag to reorder',
'caption' => 'Caption',
'write_caption' => 'Write your caption...',
'content_exceeds_platform' => ':platform: too long by :over chars (max :limit).',
2026-04-02 20:57:06 +00:00
'tiktok' => [
'settings' => 'TikTok Settings',
feat(tiktok): photo carousel support + UX Content Sharing API compliance ## Photo carousel support - Adds `ContentType::TikTokPhoto` enum case (max 35 photos, 1:1 aspect, supportsImage true, supportsVideo false) and JS mirror in content-type.ts. - Variant pill picker (Video / Photo carousel) at the top of TikTokSettings, mirroring the Instagram pattern. Wired through ScheduleTab to the parent editor's existing update:platformContentType emit. - i18n keys for variant_label / variant.video / variant.photo in en/pt-BR/es. - Publisher: split buildPostInfo into buildVideoPostInfo (uses `title`, TikTok cap 2200 chars) and buildPhotoPostInfo (uses `description`, cap 4000 chars; omits Duet/Stitch/AIGC since they don't apply). Removed the no-longer-needed queryCreatorInfo() call from publishVideo/publishPhotos — its only previous consumer (silent privacy_level fallback) is gone. ## UX Content Sharing API compliance Per TikTok review feedback citing https://developers.tiktok.com/doc/content-sharing-guidelines#required_ux_implementation_in_your_app Point 1 — already satisfied (creator_info fetch + nickname display). Point 2/4 — Music Usage Confirmation declaration is now always visible in TikTokSettings; text changes between "Music Usage Confirmation" and "Branded Content Policy and Music Usage Confirmation" based on toggle state. Previously the entire `<p>` block was conditional on a brand toggle being selected, hiding the baseline declaration. Point 2b — privacy_level may not have a default. UI was already correct; backend hardened: UpdatePostRequest now requires meta.privacy_level for tiktok platforms when status is publishing/scheduled (via withValidator); TikTokPublisher::resolveRequiredPrivacyLevel throws TikTokPublishException (ContentPolicy category) when missing instead of silently falling back to the creator's preferred level. Point 2c — interaction settings now condition on content type: - Photo posts hide Duet/Stitch (they don't apply per TikTok docs). - Photo posts hide AIGC (also video-only). - Video posts hide Auto Add Music (photos-only feature). - Max-duration warning hidden when not a video post. Source of truth is the user-selected contentType prop, not inferred from media — ensures the UI reacts immediately to the variant pill. Point 3a — publish button stays disabled when Disclose toggle is on without a sub-selection (already the case via tiktokComplianceValid). The disabled tooltip now uses the verbatim TikTok-required text "You need to indicate if your content promotes yourself, a third party, or both." instead of the generic "Some platform settings are incomplete..." when the only blocker is TikTok disclosure incompleteness. Point 3b — SELF_ONLY (Only me) privacy option is no longer filtered out when Branded Content is checked. It is rendered disabled with a hover tooltip "Branded content visibility cannot be set to private." plus a persistent amber warning paragraph below the dropdown. When the user toggles Branded Content while privacy is SELF_ONLY, the privacy clears and a vue-sonner warning toast surfaces the change. ## Cross-cutting - New `resources/js/enums/platform.ts` mirrors the PHP Platform enum, used in Edit.vue (tiktokComplianceValid + tiktokDisclosureIncomplete) and ScheduleTab.vue (all selected*Platforms computeds) to replace string literal comparisons against `'tiktok'` / `'facebook'` / etc. - PostPlatformFactory tiktok() state defaults meta.privacy_level to SELF_ONLY so existing test fixtures keep passing under the new publisher/FormRequest requirements. ## Tests - New tests/Unit/Enums/PostPlatform/TikTokPhotoContentTypeTest.php covering the new enum case (4 tests). - TikTokPublisherTest: added "video uses title not description" and "throws when meta.privacy_level missing" regression tests; renamed two existing tests that depended on the removed silent fallback. - New tests/Feature/UpdatePostRequestTest.php with 3 tests covering the FormRequest's privacy_level enforcement (publish-rejected, publish-passes, draft-allowed). Full Pest suite: 1490 passed, 2 skipped (pre-existing).
2026-05-09 15:47:49 +00:00
'variant_label' => 'Post type',
'variant' => [
'video' => 'Video',
'photo' => 'Photo carousel',
],
2026-04-23 16:23:24 +00:00
'posting_to' => 'Posting to',
2026-04-02 20:57:06 +00:00
'privacy_level' => 'Who can see this video?',
'privacy_placeholder' => 'Select who can view this post',
2026-04-02 20:57:06 +00:00
'privacy' => [
'public' => 'Public to everyone',
'friends' => 'Mutual follow friends',
'followers' => 'Followers',
'private' => 'Only me',
feat(tiktok): photo carousel support + UX Content Sharing API compliance ## Photo carousel support - Adds `ContentType::TikTokPhoto` enum case (max 35 photos, 1:1 aspect, supportsImage true, supportsVideo false) and JS mirror in content-type.ts. - Variant pill picker (Video / Photo carousel) at the top of TikTokSettings, mirroring the Instagram pattern. Wired through ScheduleTab to the parent editor's existing update:platformContentType emit. - i18n keys for variant_label / variant.video / variant.photo in en/pt-BR/es. - Publisher: split buildPostInfo into buildVideoPostInfo (uses `title`, TikTok cap 2200 chars) and buildPhotoPostInfo (uses `description`, cap 4000 chars; omits Duet/Stitch/AIGC since they don't apply). Removed the no-longer-needed queryCreatorInfo() call from publishVideo/publishPhotos — its only previous consumer (silent privacy_level fallback) is gone. ## UX Content Sharing API compliance Per TikTok review feedback citing https://developers.tiktok.com/doc/content-sharing-guidelines#required_ux_implementation_in_your_app Point 1 — already satisfied (creator_info fetch + nickname display). Point 2/4 — Music Usage Confirmation declaration is now always visible in TikTokSettings; text changes between "Music Usage Confirmation" and "Branded Content Policy and Music Usage Confirmation" based on toggle state. Previously the entire `<p>` block was conditional on a brand toggle being selected, hiding the baseline declaration. Point 2b — privacy_level may not have a default. UI was already correct; backend hardened: UpdatePostRequest now requires meta.privacy_level for tiktok platforms when status is publishing/scheduled (via withValidator); TikTokPublisher::resolveRequiredPrivacyLevel throws TikTokPublishException (ContentPolicy category) when missing instead of silently falling back to the creator's preferred level. Point 2c — interaction settings now condition on content type: - Photo posts hide Duet/Stitch (they don't apply per TikTok docs). - Photo posts hide AIGC (also video-only). - Video posts hide Auto Add Music (photos-only feature). - Max-duration warning hidden when not a video post. Source of truth is the user-selected contentType prop, not inferred from media — ensures the UI reacts immediately to the variant pill. Point 3a — publish button stays disabled when Disclose toggle is on without a sub-selection (already the case via tiktokComplianceValid). The disabled tooltip now uses the verbatim TikTok-required text "You need to indicate if your content promotes yourself, a third party, or both." instead of the generic "Some platform settings are incomplete..." when the only blocker is TikTok disclosure incompleteness. Point 3b — SELF_ONLY (Only me) privacy option is no longer filtered out when Branded Content is checked. It is rendered disabled with a hover tooltip "Branded content visibility cannot be set to private." plus a persistent amber warning paragraph below the dropdown. When the user toggles Branded Content while privacy is SELF_ONLY, the privacy clears and a vue-sonner warning toast surfaces the change. ## Cross-cutting - New `resources/js/enums/platform.ts` mirrors the PHP Platform enum, used in Edit.vue (tiktokComplianceValid + tiktokDisclosureIncomplete) and ScheduleTab.vue (all selected*Platforms computeds) to replace string literal comparisons against `'tiktok'` / `'facebook'` / etc. - PostPlatformFactory tiktok() state defaults meta.privacy_level to SELF_ONLY so existing test fixtures keep passing under the new publisher/FormRequest requirements. ## Tests - New tests/Unit/Enums/PostPlatform/TikTokPhotoContentTypeTest.php covering the new enum case (4 tests). - TikTokPublisherTest: added "video uses title not description" and "throws when meta.privacy_level missing" regression tests; renamed two existing tests that depended on the removed silent fallback. - New tests/Feature/UpdatePostRequestTest.php with 3 tests covering the FormRequest's privacy_level enforcement (publish-rejected, publish-passes, draft-allowed). Full Pest suite: 1490 passed, 2 skipped (pre-existing).
2026-05-09 15:47:49 +00:00
'private_disabled_branded' => 'Branded content visibility cannot be set to private.',
2026-04-02 20:57:06 +00:00
],
'privacy_hint' => 'The available options depend on your TikTok account settings.',
'auto_add_music' => 'Auto add music',
'auto_add_music_hint' => 'This feature is available only for photos. It will add a default music that you can change later.',
'yes' => 'Yes',
'no' => 'No',
'allow_users' => 'Allow users to:',
'comments' => 'Comment',
'duet' => 'Duet',
'stitch' => 'Stitch',
'is_aigc' => 'Video made with AI',
2026-04-23 16:23:24 +00:00
'disclose' => 'Disclose video content',
'disclose_hint' => 'Turn on to disclose that this video promotes goods or services in exchange for something of value. Your video could promote yourself, a third party, or both.',
'promotional_organic_title' => 'Your photo/video will be labeled as "Promotional content".',
'promotional_paid_title' => 'Your photo/video will be labeled as "Paid partnership".',
'promotional_description' => 'This cannot be changed once your video is posted.',
'compliance_incomplete' => 'You need to indicate if your content promotes yourself, a third party, or both.',
'privacy_required' => 'TikTok privacy level is required when publishing.',
feat(tiktok): photo carousel support + UX Content Sharing API compliance ## Photo carousel support - Adds `ContentType::TikTokPhoto` enum case (max 35 photos, 1:1 aspect, supportsImage true, supportsVideo false) and JS mirror in content-type.ts. - Variant pill picker (Video / Photo carousel) at the top of TikTokSettings, mirroring the Instagram pattern. Wired through ScheduleTab to the parent editor's existing update:platformContentType emit. - i18n keys for variant_label / variant.video / variant.photo in en/pt-BR/es. - Publisher: split buildPostInfo into buildVideoPostInfo (uses `title`, TikTok cap 2200 chars) and buildPhotoPostInfo (uses `description`, cap 4000 chars; omits Duet/Stitch/AIGC since they don't apply). Removed the no-longer-needed queryCreatorInfo() call from publishVideo/publishPhotos — its only previous consumer (silent privacy_level fallback) is gone. ## UX Content Sharing API compliance Per TikTok review feedback citing https://developers.tiktok.com/doc/content-sharing-guidelines#required_ux_implementation_in_your_app Point 1 — already satisfied (creator_info fetch + nickname display). Point 2/4 — Music Usage Confirmation declaration is now always visible in TikTokSettings; text changes between "Music Usage Confirmation" and "Branded Content Policy and Music Usage Confirmation" based on toggle state. Previously the entire `<p>` block was conditional on a brand toggle being selected, hiding the baseline declaration. Point 2b — privacy_level may not have a default. UI was already correct; backend hardened: UpdatePostRequest now requires meta.privacy_level for tiktok platforms when status is publishing/scheduled (via withValidator); TikTokPublisher::resolveRequiredPrivacyLevel throws TikTokPublishException (ContentPolicy category) when missing instead of silently falling back to the creator's preferred level. Point 2c — interaction settings now condition on content type: - Photo posts hide Duet/Stitch (they don't apply per TikTok docs). - Photo posts hide AIGC (also video-only). - Video posts hide Auto Add Music (photos-only feature). - Max-duration warning hidden when not a video post. Source of truth is the user-selected contentType prop, not inferred from media — ensures the UI reacts immediately to the variant pill. Point 3a — publish button stays disabled when Disclose toggle is on without a sub-selection (already the case via tiktokComplianceValid). The disabled tooltip now uses the verbatim TikTok-required text "You need to indicate if your content promotes yourself, a third party, or both." instead of the generic "Some platform settings are incomplete..." when the only blocker is TikTok disclosure incompleteness. Point 3b — SELF_ONLY (Only me) privacy option is no longer filtered out when Branded Content is checked. It is rendered disabled with a hover tooltip "Branded content visibility cannot be set to private." plus a persistent amber warning paragraph below the dropdown. When the user toggles Branded Content while privacy is SELF_ONLY, the privacy clears and a vue-sonner warning toast surfaces the change. ## Cross-cutting - New `resources/js/enums/platform.ts` mirrors the PHP Platform enum, used in Edit.vue (tiktokComplianceValid + tiktokDisclosureIncomplete) and ScheduleTab.vue (all selected*Platforms computeds) to replace string literal comparisons against `'tiktok'` / `'facebook'` / etc. - PostPlatformFactory tiktok() state defaults meta.privacy_level to SELF_ONLY so existing test fixtures keep passing under the new publisher/FormRequest requirements. ## Tests - New tests/Unit/Enums/PostPlatform/TikTokPhotoContentTypeTest.php covering the new enum case (4 tests). - TikTokPublisherTest: added "video uses title not description" and "throws when meta.privacy_level missing" regression tests; renamed two existing tests that depended on the removed silent fallback. - New tests/Feature/UpdatePostRequestTest.php with 3 tests covering the FormRequest's privacy_level enforcement (publish-rejected, publish-passes, draft-allowed). Full Pest suite: 1490 passed, 2 skipped (pre-existing).
2026-05-09 15:47:49 +00:00
'branded_cleared_private' => 'Privacy was cleared because Branded Content cannot be private.',
'interaction_disabled_by_creator' => 'Turned off in your TikTok account settings',
2026-04-23 16:23:24 +00:00
'max_duration_exceeded' => 'Video is :duration s long but this account can only post videos up to :max s.',
'processing_hint' => 'After publishing, it may take a few minutes for the content to process and appear on your TikTok profile.',
'brand_organic' => 'Your brand',
'brand_organic_hint' => 'You are promoting yourself or your own brand. This video will be classified as Brand Organic.',
'brand_content' => 'Branded content',
'brand_content_hint' => 'You are promoting another brand or a third party. This video will be classified as Branded Content.',
'compliance' => [
'agree' => "By posting, you agree to TikTok's",
'music_usage' => 'Music Usage Confirmation',
'and' => 'and',
'branded_policy' => 'Branded Content Policy',
],
2026-04-02 20:57:06 +00:00
],
'instagram' => [
'settings' => 'Instagram Settings',
'posting_to' => 'Posting to',
'variant_label' => 'Post type',
'variant' => [
'feed' => 'Feed Post',
'reel' => 'Reel',
'story' => 'Story',
],
'aspect_label' => 'Aspect ratio',
'aspect' => [
'square' => 'Square (1:1)',
'portrait' => 'Portrait (4:5)',
'landscape' => 'Landscape (16:9)',
'original' => 'Original',
],
],
'facebook' => [
'settings' => 'Facebook Settings',
'posting_to' => 'Posting to',
'variant_label' => 'Post type',
'variant' => [
'post' => 'Post',
'reel' => 'Reel',
'story' => 'Story',
],
'aspect_label' => 'Aspect ratio',
'aspect' => [
'square' => 'Square (1:1)',
'portrait' => 'Portrait (4:5)',
'landscape' => 'Landscape (16:9)',
'original' => 'Original',
],
],
'linkedin' => [
'settings' => 'LinkedIn Settings',
'settings_page' => 'LinkedIn Page Settings',
'posting_to' => 'Posting to',
'document_title' => 'Document title',
'document_title_placeholder' => 'Shown on your PDF document post',
],
'pinterest' => [
'settings' => 'Pinterest Settings',
'posting_to' => 'Posting to',
'variant_label' => 'Pin type',
'variant' => [
'pin' => 'Pin',
'video_pin' => 'Video Pin',
'carousel' => 'Carousel',
],
'board' => 'Board',
'select_board' => 'Select a board',
'no_boards' => 'No Pinterest boards found. Create one in your Pinterest account first.',
'boards_truncated' => 'Some boards could not be loaded. If yours is missing, open Pinterest and try again.',
'search_board' => 'Search boards...',
'no_board_found' => 'No board matches your search.',
'board_required' => 'Select a Pinterest board to publish this post.',
Add optional Pinterest pin title and destination link (#232) * Add optional Pinterest pin title, description, and link. Expose title/description/link across web, API, and MCP; seed description from caption into meta on save, and publish description only from meta. Co-authored-by: Cursor <cursoragent@cursor.com> * Expand Pinterest title/description/link test coverage. Cover web draft persistence and validation bounds, API/MCP update merge and seed, and publisher payload fields on video and carousel pins. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify Pinterest: description is post content again. Keep optional title and link in meta/settings only. Remove the separate description textarea, seed logic, and meta.description path so Pinterest follows the shared caption pattern. Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor Pinterest meta handling and validation. - Update CreatePost and UpdatePost actions to filter out null values from meta fields. - Introduce a new method in PinterestPublisher to resolve board IDs, ensuring required fields are validated. - Enhance PinterestSettings component to manage title and link inputs, including validation for HTTP URLs. - Update PostPlatformMetaRules to enforce URL validation for Pinterest links. - Add tests for clearing Pinterest title and link, and for rejecting invalid links during scheduling. This refactor improves the handling of Pinterest metadata and enhances user experience by ensuring proper validation and error handling. * Add validation messages and attributes for Pinterest meta fields - Introduced custom validation messages and friendly attribute names for Pinterest link and title fields in PostPlatformMetaRules. - Updated StorePostRequest, UpdatePostRequest, and related tools to utilize these new messages and attributes. - Enhanced tests to assert correct error messages for invalid Pinterest links and title length constraints. This update improves user feedback during post creation and editing, ensuring clarity in validation errors. * Remove click.prevent directive from Pinterest link in PinterestPreview component. This change simplifies the link behavior, allowing default click actions to occur, which may enhance user interaction with the Pinterest link. * Update validation error messages for Pinterest meta fields in tests - Refined the assertions in PostApiPlatformMetaTest to include localized validation messages for Pinterest title and link fields. - Ensured that error messages reflect the updated validation rules, enhancing clarity for users during post creation and editing. --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 21:15:50 +00:00
'title' => 'Title',
'title_placeholder' => 'Optional pin title',
'link' => 'Destination link',
'link_placeholder' => 'https://example.com',
'link_invalid' => 'Enter a valid destination link (http or https).',
'title_max' => 'Title may not be longer than 100 characters.',
'link_max' => 'Destination link may not be longer than 2048 characters.',
],
'discord' => [
'settings' => 'Discord Settings',
'posting_to' => 'Posting to',
'channel' => 'Channel',
'select_channel' => 'Select a channel',
'loading_channels' => 'Loading channels…',
'search_channel' => 'Search channels…',
'no_channels' => 'No channels found.',
'channel_required' => 'Select a Discord channel to publish this post.',
'mentions' => 'Mentions',
'search_mention' => 'Mention a role or member…',
'embeds' => 'Embeds',
'embed' => 'Embed',
'add_embed' => 'Add embed',
'embed_title' => 'Embed title',
'embed_description' => 'Embed description',
'embed_url' => 'Embed URL',
'embed_image' => 'Image URL',
'embed_color' => 'Color',
],
'warnings' => [
'no_variant' => 'Pick a post type to continue.',
'requires_media' => 'This post type requires at least one image or video.',
'max_files_exceeded' => 'This post type accepts up to :max media files (you have :current).',
'min_files_required' => 'This post type requires at least :min media files (you have :current).',
'no_video_allowed' => 'This post type does not accept videos.',
'no_image_allowed' => 'This post type accepts only videos.',
'no_document_allowed' => 'This post type does not accept PDF documents.',
'no_mixed_media' => "Images and a video can't be combined in the same post.",
'document_not_alone' => 'A PDF must be posted on its own, without other images or videos.',
'gif_not_allowed' => 'This platform does not accept GIF. Remove the GIF or choose a different network.',
'image_too_large' => 'Image exceeds the :max limit for this post type (yours is :current).',
'video_too_large' => 'Video exceeds the :max limit for this post type (yours is :current).',
'document_too_large' => 'PDF exceeds the :max limit for this post type (yours is :current).',
'video_too_long' => 'Video is :current long, but this post type allows up to :max.',
'aspect_ratio_too_narrow' => 'Aspect ratio :current is too tall for this post type (min :min).',
'aspect_ratio_too_wide' => 'Aspect ratio :current is too wide for this post type (max :max).',
],
2026-01-22 00:46:27 +00:00
],
2026-01-21 23:58:08 +00:00
'status' => [
'pending' => 'Pending',
2026-01-21 23:58:08 +00:00
'draft' => 'Draft',
'scheduled' => 'Scheduled',
'publishing' => 'Publishing',
'retrying' => 'Retrying',
2026-01-21 23:58:08 +00:00
'published' => 'Published',
'partially_published' => 'Partially Published',
'failed' => 'Failed',
],
'descriptions' => [
'draft' => 'Posts waiting to be scheduled',
'scheduled' => 'Posts scheduled for publishing',
'published' => 'Posts already published',
],
2026-01-22 00:33:31 +00:00
'ai' => [
'generate' => [
'button_tooltip' => 'Generate with AI',
'title' => 'Generate post with AI',
'description' => 'Describe what the post should be about. The AI will use your brand context to write it.',
'prompt_label' => 'What is this post about?',
'prompt_placeholder' => 'e.g. Announce our new image-generation feature for carousels',
'preview_label' => 'Preview',
'start' => 'Generate',
'apply' => 'Use this content',
'retry' => 'Try again',
'cancel' => 'Cancel',
fix: subscribe to AI generation channel before dispatching the job (#269) * fix: subscribe to AI generation channel before dispatching the job AiGenerateDialog posted the generate request and only subscribed to the broadcast channel once the response came back. Reverb's plain Channel (vendor/laravel/reverb/.../Channels/Channel.php) delivers broadcasts by iterating the in-memory connection list at send time only — no history, no replay. If the queued job started streaming text_delta/stream_end events before the private-channel subscribe handshake finished, those events were gone for good and the dialog hung on 'streaming' forever. Generate the generation_id client-side, await channel subscription confirmation (Echo's channel.subscribed(), backed by pusher:subscription_succeeded) before sending the generate request, and have the backend use the client-supplied id instead of minting its own. Related to #218. * fix: handle subscribe failure, dialog-close race, and i18n hardcoded strings Code review on the previous commit surfaced real gaps: - useAiStream only handled the subscribe-success path (.subscribed()). A definitive pusher:subscription_error (expired session, CSRF mismatch) fell through to the 5s timeout and was treated as success, reproducing the exact #218 hang via a different trigger. subscribe() now returns a boolean: true on confirm-or-ambiguous-timeout (proceed optimistically), false only on an explicit error (don't dispatch work nothing will ever hear). - AiGenerateDialog now bails without dispatching if the dialog is closed while awaiting subscription confirmation (up to 5s), and unsubscribes on a post-subscribe dispatch failure so stale listeners can't flip status to 'completed' after an error was already shown. - Extracted aiGenerationChannel() so the frontend has one place to update if the channel format changes, and cross-referenced the two remaining backend copies (PostAiGenerateController, StreamPostContent). - Replaced the two hardcoded English fallback error strings with i18n keys (posts.ai.generate.errors.*) across all 16 locales — this app only ships translated copy. - Simplified subscribe(): dropped a redundant "already settled" guard and the clearTimeout bookkeeping — Promise.resolve() already no-ops after the first call, so there was nothing to guard. * fix: subscribe before dispatch in AI post creation and image regeneration flows Extracts subscribePrivateChannel as a shared helper and applies the same subscribe-before-dispatch fix from the post edit flow (issue #218) to the other two flows that stream over private channels. Post creation wizard moves the StreamPostCreation dispatch out of AiPostWizard.vue and into Loading.vue, so the channel is subscribed before the job that broadcasts on it is dispatched — previously a full page navigation happened between dispatch and subscribe with no timeout fallback, so a lost event hung the page forever. * refactor: trim explanatory comments added in the subscribe-before-dispatch fix Rationale lives in the commit history, not scattered inline comments. * refactor: trim remaining explanatory comments from the subscribe-before-dispatch fix Same cleanup as the previous commit, applied to the files from the original post-edit-flow fix. * fix: dedup post-creation dispatch, surface field-level AI errors, harden edge cases Uses ShouldBeUnique on StreamPostCreation (scoped per-user) instead of an ad-hoc cache guard, matching the pattern already used by PublishToSocialPlatform and VerifyUpcomingPostConnections. Field-level validation errors (prompt/instruction) now render inline via InputError, matching how every other form in the app surfaces them, instead of a generic status banner. Also fixes a 422 response missing the `errors` key that Inertia's client silently swallows, adds error handling around the wizard's navigation to the loading page, and extracts the duplicated error-message parsing into a shared helper.
2026-08-10 19:06:37 +00:00
'errors' => [
'start_failed' => 'Could not start generation.',
'generation_failed' => 'AI generation failed.',
],
],
'review' => [
'button_tooltip' => 'Review with AI',
'title' => 'Review post with AI',
'description' => 'AI scans for grammar, spelling, and clarity. Apply the suggestions you agree with.',
'loading' => 'Reviewing your text...',
'no_issues' => 'No issues found. Looks good.',
'original' => 'Original',
'suggestion' => 'Suggestion',
'apply' => 'Apply',
'apply_all' => 'Apply all',
'applied' => 'Applied',
'cancel' => 'Cancel',
],
'image_regenerate' => [
'button' => 'Adjust',
'title' => 'Adjust AI image',
'description' => 'Describe the correction. The new image replaces the current one and keeps its position in the carousel.',
'instruction_label' => 'Instruction',
'instruction_placeholder' => 'e.g. Fix the typo in the headline and make the background lighter.',
'processing' => 'Regenerating image... this can take a few seconds.',
'submit' => 'Regenerate image',
'cancel' => 'Cancel',
'success' => 'Image updated. The new version replaced the previous one in your post.',
'fallback_title' => 'Improve this image copy',
'errors' => [
'required' => 'Instruction is required.',
'start_failed' => 'Failed to start regeneration.',
'unavailable' => 'Unable to regenerate this image right now.',
'timeout' => 'Regeneration is taking longer than expected. Try again in a moment.',
'media_not_found' => 'Media item not found.',
'not_ai_media' => 'Only AI-generated media can be regenerated.',
],
],
'templates' => [
'image_card' => [
'name' => 'Image post',
'description' => 'AI image with a headline and caption.',
],
'carousel' => [
'name' => 'Carousel',
'description' => 'Multi-slide carousel with a caption.',
],
'tweet_card' => [
'name' => 'Tweet card',
'description' => 'Your post styled as an X/Twitter post.',
],
'tweet_card_image' => [
'name' => 'Tweet card with photo',
'description' => 'Your post as an X/Twitter card over a blurred photo.',
],
],
],
'show' => [
'title' => 'Post Details',
'edit' => 'Edit',
'back' => 'Back',
'no_content' => 'No caption',
'platforms' => 'Platforms',
'no_platforms' => 'No platforms selected.',
'view_on_platform' => 'View on platform',
'published_on' => 'Published on :date',
'scheduled_for' => 'Scheduled for :date',
'draft' => 'Draft',
'status_pending' => 'Pending',
'metrics' => 'Metrics',
'metrics_loading' => 'Loading metrics…',
'metrics_unavailable' => 'Metrics unavailable for this platform yet.',
'metrics_empty' => 'No metrics returned.',
],
2026-01-22 00:33:31 +00:00
'edit' => [
'title' => 'Edit Post',
'view_title' => 'View Post',
'labels' => 'Labels',
'no_labels' => 'No labels created yet',
2026-01-22 00:33:31 +00:00
'schedule' => 'Schedule',
'pick_time' => 'Pick time',
'pick_time_past' => 'Pick a future date and time.',
'post_now' => 'Post now',
'time' => 'Time',
'cancel' => 'Cancel',
'delete' => 'Delete',
'schedule_for' => 'Schedule for',
'schedule_date' => 'Schedule date',
'unschedule' => 'Unschedule',
2026-01-22 00:33:31 +00:00
'saving' => 'Saving...',
'saved' => 'Saved',
'draft' => 'Draft',
2026-01-22 00:33:31 +00:00
'media' => 'Media',
'add_media' => 'Add media',
2026-01-22 00:33:31 +00:00
'caption' => 'Caption',
'caption_placeholder' => 'Write your caption...',
'compose_title' => 'Create a post',
'compose_subtitle' => 'Compose your message and add media',
'preview_empty' => [
'title' => 'No platform selected',
'description' => 'Select a platform to publish to see the preview.',
],
'drop_zone_title' => 'Add media',
'drop_zone_subtitle' => 'Drag & drop files or click to browse',
'add' => 'Add',
'publish_to' => 'Publish to',
'organize' => 'Organize',
'signatures' => 'Signatures',
'view_on_platform' => 'View on platform',
'platform_status' => 'Platform status',
'compliance_incomplete' => 'Some platform settings are incomplete or incompatible with the attached media.',
'compliance' => [
'requires_content_or_media' => 'Add text or media to publish.',
'requires_media' => 'Add an image or video to publish here.',
'too_many_files' => 'Only :max file(s) allowed for this format.',
'too_few_files' => 'Add at least :min files for this format.',
'no_videos' => 'Only images are allowed for this format.',
'no_images' => 'Only videos are allowed for this format.',
'no_mixed_media' => "Images and videos can't be combined in the same post.",
'no_gifs' => 'GIFs are not supported here.',
'no_documents' => 'PDF documents are not supported by this format.',
'document_not_alone' => 'A PDF must be posted on its own, without other images or videos.',
'video_too_large' => 'Video exceeds the size limit for this platform.',
'video_too_long' => 'Video must be under :seconds seconds for this format.',
'image_too_large' => 'Image exceeds the size limit for this platform.',
'document_too_large' => 'PDF exceeds the size limit for this platform.',
'aspect_ratio_invalid' => 'Aspect ratio is not supported by this format.',
'no_content_type' => 'Pick a content type for this platform.',
'requires_text' => 'Add text — this format needs a title.',
],
'publishing' => 'Publishing...',
'publishing_overlay_title' => 'Your post is being published',
'publishing_overlay_subtitle' => 'This can take a few moments. You can safely leave this page.',
'scheduled_overlay_title' => 'This post is scheduled',
'scheduled_overlay_subtitle' => 'Scheduled for :date. Unschedule it first to make changes.',
'unschedule_cta' => 'Unschedule to edit',
'tabs' => [
'compose' => 'Compose',
'preview' => 'Preview',
'channels' => 'Channels',
'comments' => 'Comments',
'comments_empty' => 'No comments yet.',
],
2026-01-22 00:33:31 +00:00
'media_picker' => [
'title' => 'Pick from gallery',
'search' => 'Search media...',
'empty' => 'No media in your gallery yet',
'cancel' => 'Cancel',
'add' => 'Add',
'add_count' => 'Add :count',
],
'alt_text' => [
'label' => 'Alt text',
'edit' => 'Edit alt text',
'placeholder' => 'Describe this image for people using screen readers',
'hint' => 'Published to platforms that support image descriptions.',
'save' => 'Save',
],
'emoji_picker' => [
'search' => 'Search emoji',
'empty' => 'No emojis found',
'recent' => 'Frequently used',
'smileys' => 'Smileys & emotion',
'people' => 'People & body',
'nature' => 'Animals & nature',
'food' => 'Food & drink',
'activities' => 'Activities',
'travel' => 'Travel & places',
'objects' => 'Objects',
'symbols' => 'Symbols',
'flags' => 'Flags',
],
'status' => [
'pending' => 'Pending',
'scheduled' => 'Scheduled',
'published' => 'Published',
'publishing' => 'Publishing...',
'retrying' => 'Retrying...',
'failed' => 'Failed',
2026-01-22 00:33:31 +00:00
],
'delete_modal' => [
'title' => 'Delete Post',
'description' => 'Are you sure you want to delete this post? This action cannot be undone.',
'action' => 'Delete',
'cancel' => 'Cancel',
],
'sync_enable' => [
2026-01-22 00:46:27 +00:00
'title' => 'Enable sync?',
'description' => 'All platforms will share the same content. Any custom edits made to individual platforms will be replaced with the current content.',
2026-01-22 00:33:31 +00:00
'cancel' => 'Cancel',
2026-01-22 00:46:27 +00:00
'action' => 'Enable sync',
2026-01-22 00:33:31 +00:00
],
'sync_disable' => [
'title' => 'Disable sync?',
'description' => 'Each platform will keep its current content, but future edits will only apply to the platform you\'re editing.',
'customize_note' => 'You\'ll be able to customize the content for each platform individually.',
'cancel' => 'Cancel',
'action' => 'Disable sync',
],
'platforms_dialog' => [
'title' => 'Select Platforms',
'description' => 'Choose which platforms to publish this post to.',
],
'signatures_modal' => [
'search' => 'Search signatures...',
'no_results' => 'No signatures found.',
],
2026-01-22 00:33:31 +00:00
'validation' => [
'select_board' => 'Select a board',
'images_not_supported' => 'Images not supported',
'videos_not_supported' => 'Videos not supported',
'max_images' => 'Max :count images',
'requires_media' => 'Requires media',
'requires_content' => 'Text content is required',
2026-01-22 00:33:31 +00:00
'exceeded' => ':count exceeded',
'does_not_support_images' => ':platform does not support images',
'supports_up_to_images' => ':platform supports up to :count images',
'does_not_support_videos' => ':platform does not support videos',
],
],
'content_types' => [
'instagram_feed' => [
'label' => 'Feed Post',
'description' => 'Appears in your feed and profile',
],
'instagram_reel' => [
'label' => 'Reel',
'description' => 'Short video up to 15 minutes',
2026-01-22 00:33:31 +00:00
],
'instagram_story' => [
'label' => 'Story',
'description' => 'Disappears after 24 hours',
],
'linkedin_post' => [
'label' => 'Post',
'description' => 'Standard post — single image, multi-image, video, or PDF',
2026-01-22 00:33:31 +00:00
],
'linkedin_page_post' => [
'label' => 'Post',
'description' => 'Standard post — single image, multi-image, video, or PDF',
],
2026-01-22 00:33:31 +00:00
'facebook_post' => [
'label' => 'Post',
'description' => 'Standard post on your page',
],
'facebook_reel' => [
'label' => 'Reel',
'description' => 'Short video up to 90 seconds',
],
'facebook_story' => [
'label' => 'Story',
'description' => 'Vertical video story, up to 60 seconds',
2026-01-22 00:33:31 +00:00
],
'tiktok_video' => [
'label' => 'Video',
'description' => 'Short-form video content',
],
'tiktok_photo' => [
'label' => 'Photo carousel',
'description' => 'Up to 35 photos as a swipeable carousel',
],
2026-01-22 00:33:31 +00:00
'youtube_short' => [
'label' => 'Short',
'description' => 'Vertical video up to 3 minutes',
2026-01-22 00:33:31 +00:00
],
'x_post' => [
'label' => 'Post',
'description' => 'Tweet with text and media',
],
'threads_post' => [
'label' => 'Post',
'description' => 'Text post with optional media',
],
'pinterest_pin' => [
'label' => 'Pin',
'description' => 'Standard image pin',
2026-01-22 00:33:31 +00:00
],
'pinterest_video_pin' => [
'label' => 'Video Pin',
'description' => 'Video pin (4s - 15min)',
2026-01-22 00:33:31 +00:00
],
'pinterest_carousel' => [
'label' => 'Carousel',
'description' => 'Multi-image carousel (2-5 images)',
2026-01-22 00:33:31 +00:00
],
'bluesky_post' => [
'label' => 'Post',
'description' => 'Text post with optional images',
],
'mastodon_post' => [
'label' => 'Post',
'description' => 'Text post with optional media',
],
'telegram_post' => [
'label' => 'Post',
'description' => 'Text post with optional media',
],
'discord_message' => [
'label' => 'Message',
'description' => 'Message to a Discord channel with optional media & embeds',
],
2026-01-22 00:33:31 +00:00
],
'platforms' => [
'linkedin' => 'LinkedIn',
'linkedin-page' => 'LinkedIn Page',
'x' => 'X',
'tiktok' => 'TikTok',
'youtube' => 'YouTube Shorts',
'facebook' => 'Facebook Page',
'instagram' => 'Instagram',
'threads' => 'Threads',
'pinterest' => 'Pinterest',
'bluesky' => 'Bluesky',
'mastodon' => 'Mastodon',
],
2026-01-22 01:08:18 +00:00
'flash' => [
'scheduled' => 'Post scheduled successfully!',
'deleted' => 'Post deleted successfully!',
'duplicated' => 'Post duplicated as a draft.',
'cannot_edit_finalized' => 'This post has already been processed and cannot be re-published. Duplicate it to try again.',
'cannot_delete_published' => 'Published posts cannot be deleted.',
2026-01-22 01:08:18 +00:00
'connect_first' => 'Connect at least one social network before creating a post.',
],
'errors' => [
'account_disconnected' => 'Social account is disconnected',
2026-03-31 00:18:07 +00:00
'account_inactive' => 'Social account is deactivated',
'account_token_expired' => 'Social account session expired — please reconnect',
fix: Pinterest video processing timeout — longer poll + retry (#246) * fix: give Pinterest video processing more time and retry on timeout A valid ~54s video pin failed after ~90s of polling while Pinterest was still processing. Extend the poll window to ~5 minutes and treat timeout as platform unavailable so PublishToSocialPlatform reschedules instead of failing the post on the first attempt. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: use Laravel Sleep for Pinterest media processing polls Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: inline Pinterest video processing poll constants Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: map Pinterest media upload statuses to an enum Use the official MediaUploadStatus values (registered, processing, succeeded, failed) instead of comparing raw strings in the publisher. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: trim Pinterest media processing docblock * fix: cap platform-unavailable retries and recover stuck retrying posts Stop infinite reschedules after 6 attempts with a user-safe failure message, keep technical detail in error_context, recover Retrying platforms in social:recover-stuck-posts, and drop unused isTerminal(). Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: remove unused failedCount in RecoverStuckPosts Co-authored-by: Cursor <cursoragent@cursor.com> * fix: skip final Pinterest poll sleep and localize recover timeout Co-authored-by: Cursor <cursoragent@cursor.com> * fix: raise publish job timeout headroom and ignore already-failed platforms Give social publish jobs 15 minutes so Pinterest media polling fits under the worker limit, bump Horizon/redis retry_after above that timeout, and skip handle/failed when the platform is already Failed so delayed jobs cannot revive posts recovered by social:recover-stuck-posts. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: restore social-publishing and ai-assistant horizon supervisors Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden Pinterest 401 handling, unique publish jobs, and recover JSON Treat media-status 401 as TokenExpired, make PublishToSocialPlatform unique per platform+attempt so retries still queue, and persist recover error_context via Eloquent casts instead of manual json_encode. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: mass-update stuck post platforms without per-row each Eloquent query updates already bind JSON arrays correctly here, so one UPDATE is enough — no manual json_encode and no N model writes. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: simplify Pinterest media processing poll loop Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: simplify publish job retry and terminal status checks Co-authored-by: Cursor <cursoragent@cursor.com> * fix: do not finalize posts while platforms are still retrying Co-authored-by: Cursor <cursoragent@cursor.com> * test: cover Pinterest timeout, unique jobs, and recover edge cases Co-authored-by: Cursor <cursoragent@cursor.com> * fix: retry Pinterest media poll on connection errors and tighten tests Co-authored-by: Cursor <cursoragent@cursor.com> * fix: remove ineffective TypeError import that breaks CI Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 23:49:37 +00:00
'platform_unavailable' => 'The platform is temporarily unavailable. We\'ll retry shortly.',
'platform_unavailable_exhausted' => 'The platform stayed unavailable after several retries. Please try again later.',
'publishing_timed_out' => 'Publishing timed out. Please try again.',
],
'delete' => [
'title' => 'Delete post?',
'description' => 'This action can\'t be undone. The post and all its media will be permanently removed.',
'confirm' => 'Yes, delete',
'cancel' => 'Cancel',
],
'create' => [
'title' => 'Create a new post',
'description' => 'Choose how you want to start.',
'scratch_title' => 'Start from scratch',
'scratch_description' => 'Open a blank post and write everything yourself.',
'ai_title' => 'Generate with AI',
'ai_description' => 'Describe what you want and AI generates the content for you.',
'ai_configure_description' => 'Pick a format and describe the post you want to create.',
'ai_pick_template_description' => 'Choose a style for your AI-generated post.',
'coming_soon' => 'Coming soon',
'preview' => [
'image_title' => 'Image title',
'image_body' => 'Image body',
],
'steps' => [
'template_picker_title' => 'Choose a style',
'format_title' => 'Choose a format',
'format_description' => 'Select the type of post you want to create.',
'account_title' => 'Choose an account',
'account_description' => 'Select the social account to publish to.',
'media_title' => 'Media options',
'media_carousel' => 'How many slides?',
'media_optional' => 'Include images?',
'media_optional_label' => 'How many images?',
'media_none' => 'None',
'media_count_label' => 'Number of images',
'brand_colors_label' => 'Brand colors',
'brand_colors_description' => 'Apply your workspace brand palette to the generated images. Turn it off to let the AI pick colors that fit the content.',
'prompt_title' => 'Describe your post',
'prompt_label' => 'What is this post about?',
'prompt_placeholder' => 'e.g. Announce our new carousel feature for Instagram',
'preview_error' => 'Something went wrong. Please try again.',
'loading_page_title' => 'Generating your post',
'loading_eta' => 'Estimated time: about :minutes.',
'loading_eta_minute_one' => '1 minute',
'loading_eta_minute_other' => ':count minutes',
'loading_leave_title' => 'You can keep working.',
'loading_leave_body' => 'We will notify you when the post is ready.',
'loading_leave_cta' => 'Go to calendar',
'loading_create_another_cta' => 'Create another post',
'loading_tip_credits' => 'Each AI image uses about 15 credits.',
'loading_tip_edit' => 'You will be able to edit everything once the post is ready.',
'loading_tip_draft' => 'Generated posts land in your drafts.',
'loading_tip_brand' => 'Tweak your brand settings to influence future posts.',
'loading_tip_carousel' => 'Carousels deliver one slide per uploaded image.',
'loading_tip_quality' => 'Image quality is set to balance speed and cost.',
'create' => 'Create post',
'back' => 'Back',
'next' => 'Continue',
'cancel' => 'Cancel',
'discard' => 'Discard',
'retry' => 'Try again',
'no_platforms' => 'No connected accounts',
'connect_first' => 'Connect at least one social account to use AI generation.',
'no_account_for_template' => 'Connect a compatible account to use this template.',
'format' => [
'instagram_feed' => 'Instagram Feed Post',
'instagram_carousel' => 'Instagram Carousel',
'linkedin_post' => 'LinkedIn Post',
'linkedin_page_post' => 'LinkedIn Page Post',
'x_post' => 'X Post',
'bluesky_post' => 'Bluesky Post',
'threads_post' => 'Threads Post',
'mastodon_post' => 'Mastodon Post',
'telegram_post' => 'Telegram Post',
'discord_message' => 'Discord Message',
'facebook_post' => 'Facebook Post',
'pinterest_pin' => 'Pinterest Pin',
'instagram_story' => 'Instagram Story',
'facebook_story' => 'Facebook Story',
],
],
],
2026-01-21 23:58:08 +00:00
];