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' ,
2026-05-02 15:22:42 +00:00
'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.' ,
feat(posts): multi-select label filter on the posts list
Adds a combobox-style filter to the posts index toolbar so users can
narrow All / Scheduled / Posted / Drafts views by one or more labels.
- `PostController::index` accepts `?labels[]=<id>` and applies
`whereHas('labels', whereIn(...))` (OR semantics across selected labels).
Workspace labels are exposed to the page (sorted by name) and the
selected set comes back under `filters.labels`.
- New `LabelFilter.vue` component reuses the existing Popover + Command
pattern (matching `FontPicker` in the Brand settings page). Trigger
renders the selected `LabelBadge`s inline (mirroring how each post row
already displays its labels): 1-3 shown directly, 4+ shown as the
first three plus a "+N" overflow indicator. Clear button has a
tooltip and `cursor-pointer`, and stops `click`/`pointerdown`/
`mousedown` so it doesn't reopen the Popover.
- Existing search debounce is shared with the new label watcher via a
single `buildFilterUrl` helper. URL is updated with `preserveState +
replace` so the back stack stays clean.
- i18n in en / pt-BR / es: `filter_by_label`, `label_search_placeholder`,
`no_labels`, `clear_label_filter`.
Tests: 4 new index tests covering the labels prop exposure, single-label
filter, multi-label OR filter, and blank-id sanitization. Full suite:
1509 passed, 2 skipped, 0 failed.
2026-05-14 12:57:55 +00:00
'filter_by_label' => 'Filter by label' ,
'label_search_placeholder' => 'Search labels...' ,
'no_labels' => 'No labels found.' ,
'clear_label_filter' => 'Clear label filter' ,
2026-05-02 15:22:42 +00:00
'table' => [
2026-05-06 17:26:18 +00:00
'post' => 'Post' ,
2026-05-02 15:22:42 +00:00
'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' ,
2026-05-04 01:37:51 +00:00
'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...' ,
2026-05-11 22:39:41 +00:00
'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?' ,
2026-05-29 13:16:04 +00:00
'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.' ,
2026-05-09 16:56:00 +00:00
'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.' ,
2026-05-29 13:16:04 +00:00
'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
],
feat: media gallery picker, custom emoji picker, preview tabs, real platform logos
- gallery: extract /assets tabs (uploads, Unsplash, Giphy) into shared
GalleryBrowser used by both /assets and a new MediaPickerDialog inside the
post editor; add JSON search endpoint for workspace assets with tests
- emoji: replace broken emoji-picker-element web component with a custom
EmojiPicker (full Unicode set, search, categories, recently-used,
light/dark, i18n)
- preview tab: platform selector pills, variant tabs (data-driven from
content_types map) so the user can switch Feed/Reel/Story etc. and have
it autosave through the same handler ScheduleTab uses
- platform logos: shared usePlatformLogo composable (logo + label + content
types); replaces inline maps across 5 components, fixes
instagram-facebook falling back to default.png
- tooltips: hover details (display_name · @username + platform label) on
platform avatars across editor, posts list and calendar
- settings cards: show ` · @username` in the title bar so multiple accounts
on the same network are distinguishable
- routes: drop the throttle:6,1 group middleware on social connect routes
(was 429ing legitimate OAuth retries) and rely on the default limiter
2026-05-01 17:53:49 +00:00
'instagram' => [
'settings' => 'Instagram Settings' ,
'posting_to' => 'Posting to' ,
'variant_label' => 'Post type' ,
'variant' => [
'feed' => 'Feed Post' ,
'reel' => 'Reel' ,
'story' => 'Story' ,
],
2026-05-02 15:22:42 +00:00
'aspect_label' => 'Aspect ratio' ,
'aspect' => [
'square' => 'Square (1:1)' ,
'portrait' => 'Portrait (4:5)' ,
'landscape' => 'Landscape (16:9)' ,
'original' => 'Original' ,
],
feat: media gallery picker, custom emoji picker, preview tabs, real platform logos
- gallery: extract /assets tabs (uploads, Unsplash, Giphy) into shared
GalleryBrowser used by both /assets and a new MediaPickerDialog inside the
post editor; add JSON search endpoint for workspace assets with tests
- emoji: replace broken emoji-picker-element web component with a custom
EmojiPicker (full Unicode set, search, categories, recently-used,
light/dark, i18n)
- preview tab: platform selector pills, variant tabs (data-driven from
content_types map) so the user can switch Feed/Reel/Story etc. and have
it autosave through the same handler ScheduleTab uses
- platform logos: shared usePlatformLogo composable (logo + label + content
types); replaces inline maps across 5 components, fixes
instagram-facebook falling back to default.png
- tooltips: hover details (display_name · @username + platform label) on
platform avatars across editor, posts list and calendar
- settings cards: show ` · @username` in the title bar so multiple accounts
on the same network are distinguishable
- routes: drop the throttle:6,1 group middleware on social connect routes
(was 429ing legitimate OAuth retries) and rely on the default limiter
2026-05-01 17:53:49 +00:00
],
'facebook' => [
'settings' => 'Facebook Settings' ,
'posting_to' => 'Posting to' ,
'variant_label' => 'Post type' ,
'variant' => [
'post' => 'Post' ,
'reel' => 'Reel' ,
'story' => 'Story' ,
],
2026-06-05 21:01:45 +00:00
'aspect_label' => 'Aspect ratio' ,
'aspect' => [
'square' => 'Square (1:1)' ,
'portrait' => 'Portrait (4:5)' ,
'landscape' => 'Landscape (16:9)' ,
'original' => 'Original' ,
],
feat: media gallery picker, custom emoji picker, preview tabs, real platform logos
- gallery: extract /assets tabs (uploads, Unsplash, Giphy) into shared
GalleryBrowser used by both /assets and a new MediaPickerDialog inside the
post editor; add JSON search endpoint for workspace assets with tests
- emoji: replace broken emoji-picker-element web component with a custom
EmojiPicker (full Unicode set, search, categories, recently-used,
light/dark, i18n)
- preview tab: platform selector pills, variant tabs (data-driven from
content_types map) so the user can switch Feed/Reel/Story etc. and have
it autosave through the same handler ScheduleTab uses
- platform logos: shared usePlatformLogo composable (logo + label + content
types); replaces inline maps across 5 components, fixes
instagram-facebook falling back to default.png
- tooltips: hover details (display_name · @username + platform label) on
platform avatars across editor, posts list and calendar
- settings cards: show ` · @username` in the title bar so multiple accounts
on the same network are distinguishable
- routes: drop the throttle:6,1 group middleware on social connect routes
(was 429ing legitimate OAuth retries) and rely on the default limiter
2026-05-01 17:53:49 +00:00
],
2026-05-02 17:50:36 +00:00
'linkedin' => [
'settings' => 'LinkedIn Settings' ,
'settings_page' => 'LinkedIn Page Settings' ,
'posting_to' => 'Posting to' ,
2026-06-25 00:05:09 +00:00
'document_title' => 'Document title' ,
'document_title_placeholder' => 'Shown on your PDF document post' ,
2026-05-02 17:50:36 +00:00
],
'pinterest' => [
'settings' => 'Pinterest Settings' ,
'posting_to' => 'Posting to' ,
'variant_label' => 'Pin type' ,
'variant' => [
'pin' => 'Pin' ,
'video_pin' => 'Video Pin' ,
'carousel' => 'Carousel' ,
],
2026-05-15 14:51:20 +00:00
'board' => 'Board' ,
'select_board' => 'Select a board' ,
'no_boards' => 'No Pinterest boards found. Create one in your Pinterest account first.' ,
2026-07-25 02:02:40 +00:00
'boards_truncated' => 'Some boards could not be loaded. If yours is missing, open Pinterest and try again.' ,
2026-05-15 14:51:20 +00:00
'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.' ,
2026-05-02 17:50:36 +00:00
],
feat(channels): add Discord as a social channel
Connect a Discord server via OAuth (bot authorization) and schedule/publish
messages to its channels, with mentions and rich embeds.
- Connect: custom Socialite Discord provider (bot scope) maps the authorized
guild to a SocialAccount; throws if no server was authorized.
- Publish: DiscordPublisher posts via the global bot token, validates the chosen
channel belongs to the connected guild (anti cross-guild), optimizes media,
builds allowed_mentions only from explicit mention chips (no accidental pings),
and renders rich embeds.
- Compose: per-post channel picker (live lookup), mention autocomplete and an
embed editor, gated by a required-channel compliance rule; Discord post preview.
- Enum/config/content-type wiring, ConnectionVerifier health check, throttled
lookup endpoints, i18n (en/es/pt-BR), and tests.
Operators must create a Discord application and set DISCORD_CLIENT_ID,
DISCORD_CLIENT_SECRET, DISCORD_BOT_TOKEN and DISCORD_CLIENT_REDIRECT.
2026-06-16 17:44:00 +00:00
'discord' => [
'settings' => 'Discord Settings' ,
'posting_to' => 'Posting to' ,
'channel' => 'Channel' ,
'select_channel' => 'Select a channel' ,
'loading_channels' => 'Loading channels…' ,
2026-06-16 18:53:20 +00:00
'search_channel' => 'Search channels…' ,
'no_channels' => 'No channels found.' ,
feat(channels): add Discord as a social channel
Connect a Discord server via OAuth (bot authorization) and schedule/publish
messages to its channels, with mentions and rich embeds.
- Connect: custom Socialite Discord provider (bot scope) maps the authorized
guild to a SocialAccount; throws if no server was authorized.
- Publish: DiscordPublisher posts via the global bot token, validates the chosen
channel belongs to the connected guild (anti cross-guild), optimizes media,
builds allowed_mentions only from explicit mention chips (no accidental pings),
and renders rich embeds.
- Compose: per-post channel picker (live lookup), mention autocomplete and an
embed editor, gated by a required-channel compliance rule; Discord post preview.
- Enum/config/content-type wiring, ConnectionVerifier health check, throttled
lookup endpoints, i18n (en/es/pt-BR), and tests.
Operators must create a Discord application and set DISCORD_CLIENT_ID,
DISCORD_CLIENT_SECRET, DISCORD_BOT_TOKEN and DISCORD_CLIENT_REDIRECT.
2026-06-16 17:44:00 +00:00
'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' ,
],
feat: media gallery picker, custom emoji picker, preview tabs, real platform logos
- gallery: extract /assets tabs (uploads, Unsplash, Giphy) into shared
GalleryBrowser used by both /assets and a new MediaPickerDialog inside the
post editor; add JSON search endpoint for workspace assets with tests
- emoji: replace broken emoji-picker-element web component with a custom
EmojiPicker (full Unicode set, search, categories, recently-used,
light/dark, i18n)
- preview tab: platform selector pills, variant tabs (data-driven from
content_types map) so the user can switch Feed/Reel/Story etc. and have
it autosave through the same handler ScheduleTab uses
- platform logos: shared usePlatformLogo composable (logo + label + content
types); replaces inline maps across 5 components, fixes
instagram-facebook falling back to default.png
- tooltips: hover details (display_name · @username + platform label) on
platform avatars across editor, posts list and calendar
- settings cards: show ` · @username` in the title bar so multiple accounts
on the same network are distinguishable
- routes: drop the throttle:6,1 group middleware on social connect routes
(was 429ing legitimate OAuth retries) and rely on the default limiter
2026-05-01 17:53:49 +00:00
'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.' ,
feat(linkedin): support PDF document (carousel) posts
Add LinkedIn document posts — the swipeable PDF carousel — for both personal profiles and company pages. This is the format every major competitor exposes via native PDF upload, and the reason a trial user churned.
- New 'document' media type (application/pdf) across the upload pipeline (Type enum, HasMedia, FormRequests incl. chunked, Platform media types)
- New LinkedInDocument / LinkedInPageDocument content types: PDF-only, single-file, with a supportsDocument() flag
- Publisher flow: documents initializeUpload -> PUT -> poll AVAILABLE -> post with content.media.{id,title}; optional document_title meta (falls back to file name)
- PDF is mutually exclusive with image/video, enforced in ContentTypeCompatibleWithMedia
- Frontend: 'Document (PDF)' variant, media rules (100MB cap), composer/gallery/detail PDF cards, real PDF embed in the LinkedIn editor preview, i18n in en/es/pt-BR
- Tests: publishers (personal + page, incl. processing-failure path), enums, compatibility rule, chunked PDF upload, API + MCP document_title round-trip
LinkedIn caps documents at 100MB / 300 pages (Documents API). The page limit is enforced by LinkedIn at publish, not validated client-side.
2026-06-24 19:32:27 +00:00
'no_document_allowed' => 'This post type does not accept PDF documents.' ,
2026-06-25 00:05:09 +00:00
'no_mixed_media' => " Images and a video can't be combined in the same post. " ,
2026-06-25 12:45:15 +00:00
'document_not_alone' => 'A PDF must be posted on its own, without other images or videos.' ,
feat: media gallery picker, custom emoji picker, preview tabs, real platform logos
- gallery: extract /assets tabs (uploads, Unsplash, Giphy) into shared
GalleryBrowser used by both /assets and a new MediaPickerDialog inside the
post editor; add JSON search endpoint for workspace assets with tests
- emoji: replace broken emoji-picker-element web component with a custom
EmojiPicker (full Unicode set, search, categories, recently-used,
light/dark, i18n)
- preview tab: platform selector pills, variant tabs (data-driven from
content_types map) so the user can switch Feed/Reel/Story etc. and have
it autosave through the same handler ScheduleTab uses
- platform logos: shared usePlatformLogo composable (logo + label + content
types); replaces inline maps across 5 components, fixes
instagram-facebook falling back to default.png
- tooltips: hover details (display_name · @username + platform label) on
platform avatars across editor, posts list and calendar
- settings cards: show ` · @username` in the title bar so multiple accounts
on the same network are distinguishable
- routes: drop the throttle:6,1 group middleware on social connect routes
(was 429ing legitimate OAuth retries) and rely on the default limiter
2026-05-01 17:53:49 +00:00
'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).' ,
feat(linkedin): support PDF document (carousel) posts
Add LinkedIn document posts — the swipeable PDF carousel — for both personal profiles and company pages. This is the format every major competitor exposes via native PDF upload, and the reason a trial user churned.
- New 'document' media type (application/pdf) across the upload pipeline (Type enum, HasMedia, FormRequests incl. chunked, Platform media types)
- New LinkedInDocument / LinkedInPageDocument content types: PDF-only, single-file, with a supportsDocument() flag
- Publisher flow: documents initializeUpload -> PUT -> poll AVAILABLE -> post with content.media.{id,title}; optional document_title meta (falls back to file name)
- PDF is mutually exclusive with image/video, enforced in ContentTypeCompatibleWithMedia
- Frontend: 'Document (PDF)' variant, media rules (100MB cap), composer/gallery/detail PDF cards, real PDF embed in the LinkedIn editor preview, i18n in en/es/pt-BR
- Tests: publishers (personal + page, incl. processing-failure path), enums, compatibility rule, chunked PDF upload, API + MCP document_title round-trip
LinkedIn caps documents at 100MB / 300 pages (Documents API). The page limit is enforced by LinkedIn at publish, not validated client-side.
2026-06-24 19:32:27 +00:00
'document_too_large' => 'PDF exceeds the :max limit for this post type (yours is :current).' ,
feat: media gallery picker, custom emoji picker, preview tabs, real platform logos
- gallery: extract /assets tabs (uploads, Unsplash, Giphy) into shared
GalleryBrowser used by both /assets and a new MediaPickerDialog inside the
post editor; add JSON search endpoint for workspace assets with tests
- emoji: replace broken emoji-picker-element web component with a custom
EmojiPicker (full Unicode set, search, categories, recently-used,
light/dark, i18n)
- preview tab: platform selector pills, variant tabs (data-driven from
content_types map) so the user can switch Feed/Reel/Story etc. and have
it autosave through the same handler ScheduleTab uses
- platform logos: shared usePlatformLogo composable (logo + label + content
types); replaces inline maps across 5 components, fixes
instagram-facebook falling back to default.png
- tooltips: hover details (display_name · @username + platform label) on
platform avatars across editor, posts list and calendar
- settings cards: show ` · @username` in the title bar so multiple accounts
on the same network are distinguishable
- routes: drop the throttle:6,1 group middleware on social connect routes
(was 429ing legitimate OAuth retries) and rely on the default limiter
2026-05-01 17:53:49 +00:00
'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' => [
2026-03-30 19:55:14 +00:00
'pending' => 'Pending' ,
2026-01-21 23:58:08 +00:00
'draft' => 'Draft' ,
'scheduled' => 'Scheduled' ,
'publishing' => 'Publishing' ,
2026-05-19 13:03:30 +00:00
'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
2026-05-03 12:36:50 +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' ,
],
'review' => [
'button_tooltip' => 'Review with AI' ,
'title' => 'Review post with AI' ,
2026-05-03 16:44:13 +00:00
'description' => 'AI scans for grammar, spelling, and clarity. Apply the suggestions you agree with.' ,
2026-05-03 12:36:50 +00:00
'loading' => 'Reviewing your text...' ,
'no_issues' => 'No issues found. Looks good.' ,
'original' => 'Original' ,
'suggestion' => 'Suggestion' ,
'apply' => 'Apply' ,
2026-05-03 16:44:13 +00:00
'apply_all' => 'Apply all' ,
2026-05-03 12:36:50 +00:00
'applied' => 'Applied' ,
2026-05-03 16:44:13 +00:00
'cancel' => 'Cancel' ,
2026-05-03 12:36:50 +00:00
],
2026-05-21 19:27:55 +00:00
'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.' ,
],
],
2026-06-17 20:37:51 +00:00
'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.' ,
],
2026-06-17 22:43:21 +00:00
'tweet_card_image' => [
'name' => 'Tweet card with photo' ,
'description' => 'Your post as an X/Twitter card over a blurred photo.' ,
],
2026-06-17 20:37:51 +00:00
],
2026-05-03 12:36:50 +00:00
],
2026-05-02 15:22:42 +00:00
'show' => [
'title' => 'Post Details' ,
'edit' => 'Edit' ,
refactor: in-page page headers, indies tabs, and post details polish
- Replace global AppHeader slots with in-page headers on Calendar, Edit,
and Show pages — full-width canvas with the page-specific controls
living in their own ink-bordered bar.
- Remove sidebar collapse mechanism: drop Cmd+B keyboard shortcut, force
defaultOpen=true (ignore persisted cookie), make toggleSidebar a no-op
on desktop. Sidebar is permanent on desktop, sheet behavior preserved
on mobile.
- Redesign Tabs to match the design system: each TabsTrigger renders as
a standalone sticker button (border-2 + rounded-md + shadow-xs), with
amber-200 active state mirroring the "POST" badge from post-templates.
- Show page layout: cap preview Card to max-w-xl, preserve natural
aspect for single-image posts (object-contain, max-h-[480px]), drop
redundant stats grid, swap header sides (Back button left, status +
date right with date first).
- Assets: bump GalleryBrowser search inputs to h-12 for comfortable
browsing across uploads, Unsplash, and Giphy tabs.
- Fix PostPlatformMetrics loading-forever bug: hoist useHttp out of the
onMounted callback into setup, and read the response array directly
(backend returns the array at root, not under .data).
- i18n: rename posts.show.back_to_posts → posts.show.back; remove unused
posts.show.summary.* keys.
2026-05-06 19:22:55 +00:00
'back' => 'Back' ,
2026-05-02 15:22:42 +00:00
'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' ,
2026-01-26 18:18:35 +00:00
'labels' => 'Labels' ,
2026-04-15 23:11:36 +00:00
'no_labels' => 'No labels created yet' ,
2026-01-22 00:33:31 +00:00
'schedule' => 'Schedule' ,
2026-04-15 23:11:36 +00:00
'pick_time' => 'Pick time' ,
2026-05-19 19:26:40 +00:00
'pick_time_past' => 'Pick a future date and time.' ,
2026-04-15 23:11:36 +00:00
'post_now' => 'Post now' ,
2026-04-17 02:05:51 +00:00
'time' => 'Time' ,
'cancel' => 'Cancel' ,
2026-01-26 19:16:34 +00:00
'delete' => 'Delete' ,
'schedule_for' => 'Schedule for' ,
2026-04-15 23:11:36 +00:00
'schedule_date' => 'Schedule date' ,
2026-05-06 17:26:18 +00:00
'unschedule' => 'Unschedule' ,
2026-01-22 00:33:31 +00:00
'saving' => 'Saving...' ,
'saved' => 'Saved' ,
2026-04-17 02:05:51 +00:00
'draft' => 'Draft' ,
2026-01-22 00:33:31 +00:00
'media' => 'Media' ,
2026-04-17 02:05:51 +00:00
'add_media' => 'Add media' ,
2026-01-22 00:33:31 +00:00
'caption' => 'Caption' ,
2026-04-15 23:11:36 +00:00
'caption_placeholder' => 'Write your caption...' ,
2026-04-17 02:05:51 +00:00
'compose_title' => 'Create a post' ,
'compose_subtitle' => 'Compose your message and add media' ,
2026-05-02 17:50:36 +00:00
'preview_empty' => [
'title' => 'No platform selected' ,
'description' => 'Select a platform to publish to see the preview.' ,
],
2026-05-02 18:32:55 +00:00
'drop_zone_title' => 'Add media' ,
'drop_zone_subtitle' => 'Drag & drop files or click to browse' ,
'add' => 'Add' ,
2026-04-15 23:11:36 +00:00
'publish_to' => 'Publish to' ,
'organize' => 'Organize' ,
2026-05-03 18:23:30 +00:00
'signatures' => 'Signatures' ,
2026-04-15 23:11:36 +00:00
'view_on_platform' => 'View on platform' ,
'platform_status' => 'Platform status' ,
feat: media gallery picker, custom emoji picker, preview tabs, real platform logos
- gallery: extract /assets tabs (uploads, Unsplash, Giphy) into shared
GalleryBrowser used by both /assets and a new MediaPickerDialog inside the
post editor; add JSON search endpoint for workspace assets with tests
- emoji: replace broken emoji-picker-element web component with a custom
EmojiPicker (full Unicode set, search, categories, recently-used,
light/dark, i18n)
- preview tab: platform selector pills, variant tabs (data-driven from
content_types map) so the user can switch Feed/Reel/Story etc. and have
it autosave through the same handler ScheduleTab uses
- platform logos: shared usePlatformLogo composable (logo + label + content
types); replaces inline maps across 5 components, fixes
instagram-facebook falling back to default.png
- tooltips: hover details (display_name · @username + platform label) on
platform avatars across editor, posts list and calendar
- settings cards: show ` · @username` in the title bar so multiple accounts
on the same network are distinguishable
- routes: drop the throttle:6,1 group middleware on social connect routes
(was 429ing legitimate OAuth retries) and rely on the default limiter
2026-05-01 17:53:49 +00:00
'compliance_incomplete' => 'Some platform settings are incomplete or incompatible with the attached media.' ,
2026-05-02 15:22:42 +00:00
'compliance' => [
2026-05-19 17:18:13 +00:00
'requires_content_or_media' => 'Add text or media to publish.' ,
2026-05-02 15:22:42 +00:00
'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.' ,
2026-06-23 17:30:38 +00:00
'no_mixed_media' => " Images and videos can't be combined in the same post. " ,
2026-05-02 15:22:42 +00:00
'no_gifs' => 'GIFs are not supported here.' ,
2026-06-24 20:36:39 +00:00
'no_documents' => 'PDF documents are not supported by this format.' ,
2026-06-25 12:45:15 +00:00
'document_not_alone' => 'A PDF must be posted on its own, without other images or videos.' ,
2026-05-02 15:22:42 +00:00
'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.' ,
2026-06-24 20:36:39 +00:00
'document_too_large' => 'PDF exceeds the size limit for this platform.' ,
2026-05-02 15:22:42 +00:00
'aspect_ratio_invalid' => 'Aspect ratio is not supported by this format.' ,
'no_content_type' => 'Pick a content type for this platform.' ,
2026-06-25 22:59:51 +00:00
'requires_text' => 'Add text — this format needs a title.' ,
2026-05-02 15:22:42 +00:00
],
feat: media gallery picker, custom emoji picker, preview tabs, real platform logos
- gallery: extract /assets tabs (uploads, Unsplash, Giphy) into shared
GalleryBrowser used by both /assets and a new MediaPickerDialog inside the
post editor; add JSON search endpoint for workspace assets with tests
- emoji: replace broken emoji-picker-element web component with a custom
EmojiPicker (full Unicode set, search, categories, recently-used,
light/dark, i18n)
- preview tab: platform selector pills, variant tabs (data-driven from
content_types map) so the user can switch Feed/Reel/Story etc. and have
it autosave through the same handler ScheduleTab uses
- platform logos: shared usePlatformLogo composable (logo + label + content
types); replaces inline maps across 5 components, fixes
instagram-facebook falling back to default.png
- tooltips: hover details (display_name · @username + platform label) on
platform avatars across editor, posts list and calendar
- settings cards: show ` · @username` in the title bar so multiple accounts
on the same network are distinguishable
- routes: drop the throttle:6,1 group middleware on social connect routes
(was 429ing legitimate OAuth retries) and rely on the default limiter
2026-05-01 17:53:49 +00:00
'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.' ,
2026-05-11 23:47:53 +00:00
'scheduled_overlay_title' => 'This post is scheduled' ,
'scheduled_overlay_subtitle' => 'Scheduled for :date. Unschedule it first to make changes.' ,
'unschedule_cta' => 'Unschedule to edit' ,
2026-04-15 23:11:36 +00:00
'tabs' => [
2026-07-17 21:38:20 +00:00
'compose' => 'Compose' ,
2026-04-15 23:11:36 +00:00
'preview' => 'Preview' ,
2026-06-10 19:06:02 +00:00
'channels' => 'Channels' ,
2026-04-15 23:11:36 +00:00
'comments' => 'Comments' ,
'comments_empty' => 'No comments yet.' ,
],
2026-01-22 00:33:31 +00:00
feat: media gallery picker, custom emoji picker, preview tabs, real platform logos
- gallery: extract /assets tabs (uploads, Unsplash, Giphy) into shared
GalleryBrowser used by both /assets and a new MediaPickerDialog inside the
post editor; add JSON search endpoint for workspace assets with tests
- emoji: replace broken emoji-picker-element web component with a custom
EmojiPicker (full Unicode set, search, categories, recently-used,
light/dark, i18n)
- preview tab: platform selector pills, variant tabs (data-driven from
content_types map) so the user can switch Feed/Reel/Story etc. and have
it autosave through the same handler ScheduleTab uses
- platform logos: shared usePlatformLogo composable (logo + label + content
types); replaces inline maps across 5 components, fixes
instagram-facebook falling back to default.png
- tooltips: hover details (display_name · @username + platform label) on
platform avatars across editor, posts list and calendar
- settings cards: show ` · @username` in the title bar so multiple accounts
on the same network are distinguishable
- routes: drop the throttle:6,1 group middleware on social connect routes
(was 429ing legitimate OAuth retries) and rely on the default limiter
2026-05-01 17:53:49 +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' ,
],
2026-07-11 12:45:52 +00:00
'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' ,
],
feat: media gallery picker, custom emoji picker, preview tabs, real platform logos
- gallery: extract /assets tabs (uploads, Unsplash, Giphy) into shared
GalleryBrowser used by both /assets and a new MediaPickerDialog inside the
post editor; add JSON search endpoint for workspace assets with tests
- emoji: replace broken emoji-picker-element web component with a custom
EmojiPicker (full Unicode set, search, categories, recently-used,
light/dark, i18n)
- preview tab: platform selector pills, variant tabs (data-driven from
content_types map) so the user can switch Feed/Reel/Story etc. and have
it autosave through the same handler ScheduleTab uses
- platform logos: shared usePlatformLogo composable (logo + label + content
types); replaces inline maps across 5 components, fixes
instagram-facebook falling back to default.png
- tooltips: hover details (display_name · @username + platform label) on
platform avatars across editor, posts list and calendar
- settings cards: show ` · @username` in the title bar so multiple accounts
on the same network are distinguishable
- routes: drop the throttle:6,1 group middleware on social connect routes
(was 429ing legitimate OAuth retries) and rely on the default limiter
2026-05-01 17:53:49 +00:00
'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' ,
],
2026-04-15 23:11:36 +00:00
'status' => [
2026-06-22 19:47:27 +00:00
'pending' => 'Pending' ,
2026-05-06 17:26:18 +00:00
'scheduled' => 'Scheduled' ,
2026-04-15 23:11:36 +00:00
'published' => 'Published' ,
'publishing' => 'Publishing...' ,
2026-05-19 13:03:30 +00:00
'retrying' => 'Retrying...' ,
2026-04-15 23:11:36 +00:00
'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.' ,
],
2026-05-03 18:23:30 +00:00
'signatures_modal' => [
'search' => 'Search signatures...' ,
'no_results' => 'No signatures found.' ,
2026-01-26 18:18:35 +00:00
],
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' ,
2026-04-01 18:12:56 +00:00
'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' ,
2026-07-25 00:21:39 +00:00
'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' ,
2026-06-25 00:05:09 +00:00
'description' => 'Standard post — single image, multi-image, video, or PDF' ,
2026-01-22 00:33:31 +00:00
],
'linkedin_page_post' => [
'label' => 'Post' ,
2026-06-25 00:05:09 +00:00
'description' => 'Standard post — single image, multi-image, video, or PDF' ,
feat(linkedin): support PDF document (carousel) posts
Add LinkedIn document posts — the swipeable PDF carousel — for both personal profiles and company pages. This is the format every major competitor exposes via native PDF upload, and the reason a trial user churned.
- New 'document' media type (application/pdf) across the upload pipeline (Type enum, HasMedia, FormRequests incl. chunked, Platform media types)
- New LinkedInDocument / LinkedInPageDocument content types: PDF-only, single-file, with a supportsDocument() flag
- Publisher flow: documents initializeUpload -> PUT -> poll AVAILABLE -> post with content.media.{id,title}; optional document_title meta (falls back to file name)
- PDF is mutually exclusive with image/video, enforced in ContentTypeCompatibleWithMedia
- Frontend: 'Document (PDF)' variant, media rules (100MB cap), composer/gallery/detail PDF cards, real PDF embed in the LinkedIn editor preview, i18n in en/es/pt-BR
- Tests: publishers (personal + page, incl. processing-failure path), enums, compatibility rule, chunked PDF upload, API + MCP document_title round-trip
LinkedIn caps documents at 100MB / 300 pages (Documents API). The page limit is enforced by LinkedIn at publish, not validated client-side.
2026-06-24 19:32:27 +00:00
],
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' ,
2026-05-22 00:21:11 +00:00
'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' ,
],
2026-05-09 16:56:00 +00:00
'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' ,
2026-07-23 15:06:15 +00:00
'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' ,
2026-05-09 16:56:00 +00:00
'description' => 'Standard image pin' ,
2026-01-22 00:33:31 +00:00
],
'pinterest_video_pin' => [
'label' => 'Video Pin' ,
2026-05-09 16:56:00 +00:00
'description' => 'Video pin (4s - 15min)' ,
2026-01-22 00:33:31 +00:00
],
'pinterest_carousel' => [
'label' => 'Carousel' ,
2026-05-09 16:56:00 +00:00
'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' ,
],
2026-06-14 01:10:12 +00:00
'telegram_post' => [
'label' => 'Post' ,
'description' => 'Text post with optional media' ,
],
feat(channels): add Discord as a social channel
Connect a Discord server via OAuth (bot authorization) and schedule/publish
messages to its channels, with mentions and rich embeds.
- Connect: custom Socialite Discord provider (bot scope) maps the authorized
guild to a SocialAccount; throws if no server was authorized.
- Publish: DiscordPublisher posts via the global bot token, validates the chosen
channel belongs to the connected guild (anti cross-guild), optimizes media,
builds allowed_mentions only from explicit mention chips (no accidental pings),
and renders rich embeds.
- Compose: per-post channel picker (live lookup), mention autocomplete and an
embed editor, gated by a required-channel compliance rule; Discord post preview.
- Enum/config/content-type wiring, ConnectionVerifier health check, throttled
lookup endpoints, i18n (en/es/pt-BR), and tests.
Operators must create a Discord application and set DISCORD_CLIENT_ID,
DISCORD_CLIENT_SECRET, DISCORD_BOT_TOKEN and DISCORD_CLIENT_REDIRECT.
2026-06-16 17:44:00 +00:00
'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!' ,
2026-05-04 01:37:51 +00:00
'duplicated' => 'Post duplicated as a draft.' ,
fix(facebook): empty-message rejection + state consistency + no re-publish on terminal
Production incident: a customer's Facebook Page post failed with 'The post
is empty. Please enter a message to share.' (error code 197) and ended up
with a contradictory DB state (status=published + error_message=set).
Three independent bugs were uncovered:
A. FacebookPublisher sends 'message'/'description' as null when the user
posts media without text. Graph API requires the key be omitted, not
null. Fixed in publishSingleImagePost, publishMultiImagePost,
publishVideoPost, publishReel.
B. markAsPublished/markAsFailed leak stale fields across transitions
(a published row could retain error_message from a prior failure,
vice-versa). Both transitions now explicitly clear the opposite
side's fields.
C. status='failed' was editable in the UI and the backend, so users
were re-clicking Publish, generating duplicate failure emails and
the contradictory state from bug B. The frontend isReadOnly check
and the UpdatePost backend guard now treat Published/PartiallyPublished/
Failed/Publishing as terminal. To retry, the user duplicates the post.
11 new tests guarantee these can't regress silently: FB payload shape
per content type, PostPlatform field-clearing on transitions, and the
terminal-status block at the controller level.
2026-05-15 16:01:49 +00:00
'cannot_edit_finalized' => 'This post has already been processed and cannot be re-published. Duplicate it to try again.' ,
2026-05-02 15:22:42 +00:00
'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.' ,
],
2026-01-26 13:54:29 +00:00
'errors' => [
'account_disconnected' => 'Social account is disconnected' ,
2026-03-31 00:18:07 +00:00
'account_inactive' => 'Social account is deactivated' ,
2026-05-12 21:46:06 +00:00
'account_token_expired' => 'Social account session expired — please reconnect' ,
2026-01-26 13:54:29 +00:00
],
2026-05-03 12:36:50 +00:00
2026-05-03 13:17:01 +00:00
'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' ,
],
2026-05-03 12:36:50 +00:00
'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.' ,
2026-06-17 20:42:23 +00:00
'ai_pick_template_description' => 'Choose a style for your AI-generated post.' ,
2026-05-03 12:36:50 +00:00
'template_title' => 'Use a template' ,
'template_description' => 'Pick from our curated templates and customize.' ,
'coming_soon' => 'Coming soon' ,
2026-05-03 13:06:02 +00:00
'preview' => [
'image_title' => 'Image title' ,
'image_body' => 'Image body' ,
],
2026-05-03 12:36:50 +00:00
'steps' => [
2026-06-17 20:42:23 +00:00
'template_picker_title' => 'Choose a style' ,
2026-05-03 12:36:50 +00:00
'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' ,
2026-07-18 18:12:26 +00:00
'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.' ,
2026-05-03 12:36:50 +00:00
'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.' ,
2026-05-08 21:30:11 +00:00
'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.' ,
2026-05-03 12:36:50 +00:00
'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.' ,
2026-06-17 21:09:02 +00:00
'no_account_for_template' => 'Connect a compatible account to use this template.' ,
2026-05-03 12:36:50 +00:00
'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' ,
2026-06-14 01:10:12 +00:00
'telegram_post' => 'Telegram Post' ,
feat(channels): add Discord as a social channel
Connect a Discord server via OAuth (bot authorization) and schedule/publish
messages to its channels, with mentions and rich embeds.
- Connect: custom Socialite Discord provider (bot scope) maps the authorized
guild to a SocialAccount; throws if no server was authorized.
- Publish: DiscordPublisher posts via the global bot token, validates the chosen
channel belongs to the connected guild (anti cross-guild), optimizes media,
builds allowed_mentions only from explicit mention chips (no accidental pings),
and renders rich embeds.
- Compose: per-post channel picker (live lookup), mention autocomplete and an
embed editor, gated by a required-channel compliance rule; Discord post preview.
- Enum/config/content-type wiring, ConnectionVerifier health check, throttled
lookup endpoints, i18n (en/es/pt-BR), and tests.
Operators must create a Discord application and set DISCORD_CLIENT_ID,
DISCORD_CLIENT_SECRET, DISCORD_BOT_TOKEN and DISCORD_CLIENT_REDIRECT.
2026-06-16 17:44:00 +00:00
'discord_message' => 'Discord Message' ,
2026-05-03 12:36:50 +00:00
'facebook_post' => 'Facebook Post' ,
'pinterest_pin' => 'Pinterest Pin' ,
'instagram_story' => 'Instagram Story' ,
'facebook_story' => 'Facebook Story' ,
],
],
],
'templates' => [
'browser_title' => 'Choose a template' ,
'browser_description' => 'Start from a curated template and adapt it.' ,
'search_placeholder' => 'Search templates…' ,
'no_search_results' => 'No templates match your search' ,
'try_different_search' => 'Try a different keyword or clear the search.' ,
'slides_count' => '{count} slide|{count} slides' ,
'all_platforms' => 'All platforms' ,
2026-05-06 17:26:18 +00:00
'platform_search_placeholder' => 'Search platform…' ,
'no_platform_match' => 'No platform matches.' ,
2026-05-03 12:36:50 +00:00
'use_this' => 'Use this template' ,
'no_templates' => 'No templates available.' ,
'applying' => 'Applying template…' ,
'category' => [
'product_launch' => 'Product launch' ,
'promotion' => 'Promotion' ,
'educational' => 'Educational' ,
'behind_the_scenes' => 'Behind the scenes' ,
'testimonial' => 'Testimonial' ,
'industry_tip' => 'Industry tip' ,
'event' => 'Event' ,
'engagement' => 'Engagement' ,
],
],
2026-01-21 23:58:08 +00:00
];