diff --git a/lib/src/core/l10n/app_localizations.dart b/lib/src/core/l10n/app_localizations.dart --- a/lib/src/core/l10n/app_localizations.dart +++ b/lib/src/core/l10n/app_localizations.dart @@ -439,7 +439,7 @@ /// Add description placeholder /// /// In en, this message translates to: - /// **'Add a description... (optional)'** + /// **'Add a caption... (optional)'** String get hintAddDescription; /// Add alt text placeholder @@ -651,6 +651,660 @@ /// In en, this message translates to: /// **'Other'** String get categoryOther; + + /// Continue button text + /// + /// In en, this message translates to: + /// **'Continue'** + String get buttonContinue; + + /// Get started button text + /// + /// In en, this message translates to: + /// **'Get Started'** + String get buttonGetStarted; + + /// Already have an account button text + /// + /// In en, this message translates to: + /// **'I already have an account'** + String get buttonHaveAccount; + + /// Open button text + /// + /// In en, this message translates to: + /// **'Open'** + String get buttonOpen; + + /// Post button text + /// + /// In en, this message translates to: + /// **'Post'** + String get buttonPost; + + /// Done button text + /// + /// In en, this message translates to: + /// **'Done'** + String get buttonDone; + + /// Edit button text + /// + /// In en, this message translates to: + /// **'Edit'** + String get buttonEdit; + + /// Back button text + /// + /// In en, this message translates to: + /// **'Back'** + String get buttonBack; + + /// Share button text + /// + /// In en, this message translates to: + /// **'Share'** + String get buttonShare; + + /// Copied state text for copy link + /// + /// In en, this message translates to: + /// **'Copied'** + String get buttonCopied; + + /// Copy link button text + /// + /// In en, this message translates to: + /// **'Copy link'** + String get buttonCopyLink; + + /// Error message for invalid handle + /// + /// In en, this message translates to: + /// **'Invalid handle'** + String get errorInvalidHandle; + + /// Error message for handle not found + /// + /// In en, this message translates to: + /// **'Could not find this handle'** + String get errorHandleNotFound; + + /// Loading message when completing sign in + /// + /// In en, this message translates to: + /// **'Completing sign in...'** + String get errorCompletingSignIn; + + /// Error message for profile not found + /// + /// In en, this message translates to: + /// **'Profile not found'** + String get errorProfileNotFound; + + /// Error loading post message + /// + /// In en, this message translates to: + /// **'Error loading post'** + String get errorLoadingPost; + + /// Error loading messages message + /// + /// In en, this message translates to: + /// **'Failed to load messages'** + String get errorLoadingMessages; + + /// Error loading conversations message + /// + /// In en, this message translates to: + /// **'Failed to load conversations'** + String get errorLoadingConversations; + + /// Error message when unable to open a link + /// + /// In en, this message translates to: + /// **'Unable to open link right now.'** + String get errorUnableToOpenLink; + + /// Error loading labeler settings message + /// + /// In en, this message translates to: + /// **'Error Loading Labeler Settings'** + String get errorLoadingLabelerSettings; + + /// Error message with detail + /// + /// In en, this message translates to: + /// **'Error: {error}'** + String errorWithDetail(String error); + + /// Add a comment input placeholder + /// + /// In en, this message translates to: + /// **'Add a comment...'** + String get hintAddComment; + + /// Search users placeholder in messages + /// + /// In en, this message translates to: + /// **'Search users'** + String get hintSearchUsersMessages; + + /// Delete comment confirmation message + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete this comment? This action cannot be undone.'** + String get dialogDeleteCommentConfirm; + + /// Delete post confirmation message + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete this post? This action cannot be undone.'** + String get dialogDeletePostConfirm; + + /// Block user dialog title + /// + /// In en, this message translates to: + /// **'Block User'** + String get dialogBlockUser; + + /// Block user confirmation message + /// + /// In en, this message translates to: + /// **'Are you sure you want to block this user? You will no longer see their posts.'** + String get dialogBlockUserConfirm; + + /// Unblock user dialog title + /// + /// In en, this message translates to: + /// **'Unblock User'** + String get dialogUnblockUser; + + /// Unblock user confirmation message + /// + /// In en, this message translates to: + /// **'Are you sure you want to unblock this user?'** + String get dialogUnblockUserConfirm; + + /// Remove feed confirmation message with name + /// + /// In en, this message translates to: + /// **'Are you sure you want to remove \"{name}\"?'** + String dialogRemoveFeedConfirm(String name); + + /// Dialog title for opening Bluesky account management + /// + /// In en, this message translates to: + /// **'Open Bluesky account management?'** + String get dialogOpenBlueskyAccount; + + /// Description for opening Bluesky account management dialog + /// + /// In en, this message translates to: + /// **'This opens the Bluesky account management screen. You may have to log in again.\n\nIf prompted for an account provider, use:\n{pdsUrl}'** + String dialogOpenBlueskyAccountDescription(String pdsUrl); + + /// Replies page title + /// + /// In en, this message translates to: + /// **'Replies'** + String get pageTitleReplies; + + /// Review video page title + /// + /// In en, this message translates to: + /// **'Review Video'** + String get pageTitleReviewVideo; + + /// Review image post page title + /// + /// In en, this message translates to: + /// **'Review Image Post'** + String get pageTitleReviewImagePost; + + /// Legal page title + /// + /// In en, this message translates to: + /// **'Legal'** + String get pageTitleLegal; + + /// Followers page title + /// + /// In en, this message translates to: + /// **'Followers'** + String get pageTitleFollowers; + + /// Following page title + /// + /// In en, this message translates to: + /// **'Following'** + String get pageTitleFollowing; + + /// Empty state for no videos using a sound + /// + /// In en, this message translates to: + /// **'No videos using this sound yet'** + String get emptyNoVideosUsingSound; + + /// Empty state for no photos in library + /// + /// In en, this message translates to: + /// **'No photos or videos found in your library.'** + String get emptyNoPhotoLibrary; + + /// Permission message for photo library access + /// + /// In en, this message translates to: + /// **'Allow photo library access to pick photos and videos.'** + String get messagePermissionPhotoLibrary; + + /// Posting story progress message + /// + /// In en, this message translates to: + /// **'Posting story...'** + String get messagePostingStory; + + /// Processing video progress message + /// + /// In en, this message translates to: + /// **'Processing video...'** + String get messageProcessingVideo; + + /// Uploading video status message + /// + /// In en, this message translates to: + /// **'Uploading video'** + String get messageUploadingVideo; + + /// Ready to post status message + /// + /// In en, this message translates to: + /// **'Ready to post'** + String get messageReadyToPost; + + /// Upload failed status message + /// + /// In en, this message translates to: + /// **'Upload failed'** + String get messageUploadFailed; + + /// Uploading percentage status message + /// + /// In en, this message translates to: + /// **'Uploading {percent}%'** + String messageUploadingPercent(int percent); + + /// Original sound label + /// + /// In en, this message translates to: + /// **'Original Sound'** + String get labelOriginalSound; + + /// Share label + /// + /// In en, this message translates to: + /// **'Share'** + String get labelShare; + + /// Following label + /// + /// In en, this message translates to: + /// **'Following'** + String get labelFollowing; + + /// Posts label + /// + /// In en, this message translates to: + /// **'Posts'** + String get labelPosts; + + /// Privacy policy link label + /// + /// In en, this message translates to: + /// **'Privacy Policy'** + String get labelPrivacyPolicy; + + /// Terms of service link label + /// + /// In en, this message translates to: + /// **'Terms of Service'** + String get labelTermsOfService; + + /// Support link label + /// + /// In en, this message translates to: + /// **'Support'** + String get labelSupport; + + /// Back tooltip + /// + /// In en, this message translates to: + /// **'Back'** + String get tooltipBack; + + /// Error message for failed image load + /// + /// In en, this message translates to: + /// **'Failed to load image'** + String get errorFailedToLoadImage; + + /// Sign in page title + /// + /// In en, this message translates to: + /// **'Sign In'** + String get pageTitleSignIn; + + /// Message to enter handle for OAuth + /// + /// In en, this message translates to: + /// **'Enter your handle to continue with OAuth'** + String get messageEnterHandle; + + /// Loading message when completing sign up + /// + /// In en, this message translates to: + /// **'Completing sign up...'** + String get messageCompletingSignUp; + + /// Welcome message on register page + /// + /// In en, this message translates to: + /// **'Welcome!'** + String get messageWelcome; + + /// Welcome description on register page + /// + /// In en, this message translates to: + /// **'Share videos, connect with friends,\nand take back your timeline.'** + String get messageWelcomeDescription; + + /// Reply button label + /// + /// In en, this message translates to: + /// **'Reply'** + String get labelReply; + + /// Add image tooltip + /// + /// In en, this message translates to: + /// **'Add image (1 max)'** + String get hintAddImage; + + /// Posting image progress message + /// + /// In en, this message translates to: + /// **'Posting...'** + String get messagePostingImage; + + /// Maximum images reached tooltip + /// + /// In en, this message translates to: + /// **'Maximum images reached'** + String get messageMaximumImagesReached; + + /// Sound label for video editor toolbar + /// + /// In en, this message translates to: + /// **'Sound'** + String get labelSound; + + /// Stickers label + /// + /// In en, this message translates to: + /// **'Stickers'** + String get labelStickers; + + /// Paint editor label + /// + /// In en, this message translates to: + /// **'Paint'** + String get labelPaint; + + /// Text editor label + /// + /// In en, this message translates to: + /// **'Text'** + String get labelText; + + /// Crop editor label + /// + /// In en, this message translates to: + /// **'Crop'** + String get labelCrop; + + /// Tune editor label + /// + /// In en, this message translates to: + /// **'Tune'** + String get labelTune; + + /// Filter editor label + /// + /// In en, this message translates to: + /// **'Filter'** + String get labelFilter; + + /// Blur editor label + /// + /// In en, this message translates to: + /// **'Blur'** + String get labelBlur; + + /// Emoji editor label + /// + /// In en, this message translates to: + /// **'Emoji'** + String get labelEmoji; + + /// Mention editor label + /// + /// In en, this message translates to: + /// **'Mention'** + String get labelMention; + + /// Draw editor label + /// + /// In en, this message translates to: + /// **'Draw'** + String get labelDraw; + + /// Default video clip title + /// + /// In en, this message translates to: + /// **'My awesome video'** + String get labelMyAwesomeVideo; + + /// Video upload status when uploading + /// + /// In en, this message translates to: + /// **'Uploading video'** + String get errorUploadingVideo; + + /// Video processing status + /// + /// In en, this message translates to: + /// **'Processing video'** + String get errorProcessingVideoStatus; + + /// Ready to post status + /// + /// In en, this message translates to: + /// **'Ready to post'** + String get errorReadyToPost; + + /// Upload failed status + /// + /// In en, this message translates to: + /// **'Upload failed'** + String get errorUploadFailed; + + /// Error message in snackbar + /// + /// In en, this message translates to: + /// **'Error: {error}'** + String errorSnackBar(String error); + + /// Like feed button text + /// + /// In en, this message translates to: + /// **'Like Feed'** + String get buttonLikeFeed; + + /// Unlike feed button text + /// + /// In en, this message translates to: + /// **'Unlike Feed'** + String get buttonUnlikeFeed; + + /// Empty state for no notifications + /// + /// In en, this message translates to: + /// **'No notifications'** + String get emptyNoNotifications; + + /// Message when all notifications are read + /// + /// In en, this message translates to: + /// **'You\'\'re all caught up!'** + String get messageAllCaughtUp; + + /// Description for labeler configuration + /// + /// In en, this message translates to: + /// **'Configure how this labeler\'\'s content labels are handled in your feeds.'** + String get messageLabelerConfigDescription; + + /// Error loading notifications message + /// + /// In en, this message translates to: + /// **'Failed to load notifications'** + String get errorLoadingNotifications; + + /// Content label settings title + /// + /// In en, this message translates to: + /// **'Content Label Settings'** + String get labelContentLabelSettings; + + /// Error when trying to select photos in single-select mode + /// + /// In en, this message translates to: + /// **'You can only select photos in multi-select mode.'** + String get errorPhotoSelectLimit; + + /// Error when exceeding max photo selection + /// + /// In en, this message translates to: + /// **'You can select up to {max}.'** + String errorPhotoSelectMax(int max); + + /// Error when unable to access selected photos + /// + /// In en, this message translates to: + /// **'Unable to access the selected photos.'** + String get errorUnableToAccessPhotos; + + /// Error when unable to access a media item + /// + /// In en, this message translates to: + /// **'Unable to access this media item.'** + String get errorUnableToAccessMedia; + + /// Single select mode label + /// + /// In en, this message translates to: + /// **'Single Select'** + String get labelSingleSelect; + + /// Select multiple mode label + /// + /// In en, this message translates to: + /// **'Select multiple'** + String get labelSelectMultiple; + + /// Library header label + /// + /// In en, this message translates to: + /// **'Library'** + String get labelLibrary; + + /// Done button with selection count + /// + /// In en, this message translates to: + /// **'Done ({current}/{max})'** + String labelDoneCount(int current, int max); + + /// Permission info about limited library access + /// + /// In en, this message translates to: + /// **'Limited library access is enabled. You can change this in settings.'** + String get messageLimitedLibraryAccess; + + /// Posts search tab label + /// + /// In en, this message translates to: + /// **'Posts'** + String get tabPosts; + + /// Users search tab label + /// + /// In en, this message translates to: + /// **'Users'** + String get tabUsers; + + /// Search by handle or display name placeholder + /// + /// In en, this message translates to: + /// **'Search by handle or display name'** + String get hintSearchByHandle; + + /// Feed creator attribution label + /// + /// In en, this message translates to: + /// **'by @{handle}'** + String labelFeedByCreator(String handle); + + /// Subtitle for following/timeline feed + /// + /// In en, this message translates to: + /// **'Posts from people you follow'** + String get messagePostsFromFollowing; + + /// Add feed button text + /// + /// In en, this message translates to: + /// **'Add feed'** + String get buttonAddFeed; + + /// Unpin feed button text + /// + /// In en, this message translates to: + /// **'Unpin feed'** + String get buttonUnpinFeed; + + /// Pin feed button text + /// + /// In en, this message translates to: + /// **'Pin feed'** + String get buttonPinFeed; + + /// Empty state for no conversations in share panel + /// + /// In en, this message translates to: + /// **'No conversations yet'** + String get emptyNoConversations; + + /// Sending message progress indicator + /// + /// In en, this message translates to: + /// **'Sending...'** + String get messageSending; + + /// Send button text + /// + /// In en, this message translates to: + /// **'Send'** + String get buttonSend; } class _AppLocalizationsDelegate diff --git a/lib/src/core/l10n/app_localizations_en.dart b/lib/src/core/l10n/app_localizations_en.dart --- a/lib/src/core/l10n/app_localizations_en.dart +++ b/lib/src/core/l10n/app_localizations_en.dart @@ -183,7 +183,7 @@ String get hintTypeMessage => 'Type a message...'; @override - String get hintAddDescription => 'Add a description... (optional)'; + String get hintAddDescription => 'Add a caption... (optional)'; @override String get hintAddAltText => 'Add alt text'; @@ -298,4 +298,358 @@ @override String get categoryOther => 'Other'; + + @override + String get buttonContinue => 'Continue'; + + @override + String get buttonGetStarted => 'Get Started'; + + @override + String get buttonHaveAccount => 'I already have an account'; + + @override + String get buttonOpen => 'Open'; + + @override + String get buttonPost => 'Post'; + + @override + String get buttonDone => 'Done'; + + @override + String get buttonEdit => 'Edit'; + + @override + String get buttonBack => 'Back'; + + @override + String get buttonShare => 'Share'; + + @override + String get buttonCopied => 'Copied'; + + @override + String get buttonCopyLink => 'Copy link'; + + @override + String get errorInvalidHandle => 'Invalid handle'; + + @override + String get errorHandleNotFound => 'Could not find this handle'; + + @override + String get errorCompletingSignIn => 'Completing sign in...'; + + @override + String get errorProfileNotFound => 'Profile not found'; + + @override + String get errorLoadingPost => 'Error loading post'; + + @override + String get errorLoadingMessages => 'Failed to load messages'; + + @override + String get errorLoadingConversations => 'Failed to load conversations'; + + @override + String get errorUnableToOpenLink => 'Unable to open link right now.'; + + @override + String get errorLoadingLabelerSettings => 'Error Loading Labeler Settings'; + + @override + String errorWithDetail(String error) { + return 'Error: $error'; + } + + @override + String get hintAddComment => 'Add a comment...'; + + @override + String get hintSearchUsersMessages => 'Search users'; + + @override + String get dialogDeleteCommentConfirm => + 'Are you sure you want to delete this comment? This action cannot be undone.'; + + @override + String get dialogDeletePostConfirm => + 'Are you sure you want to delete this post? This action cannot be undone.'; + + @override + String get dialogBlockUser => 'Block User'; + + @override + String get dialogBlockUserConfirm => + 'Are you sure you want to block this user? You will no longer see their posts.'; + + @override + String get dialogUnblockUser => 'Unblock User'; + + @override + String get dialogUnblockUserConfirm => + 'Are you sure you want to unblock this user?'; + + @override + String dialogRemoveFeedConfirm(String name) { + return 'Are you sure you want to remove \"$name\"?'; + } + + @override + String get dialogOpenBlueskyAccount => 'Open Bluesky account management?'; + + @override + String dialogOpenBlueskyAccountDescription(String pdsUrl) { + return 'This opens the Bluesky account management screen. You may have to log in again.\n\nIf prompted for an account provider, use:\n$pdsUrl'; + } + + @override + String get pageTitleReplies => 'Replies'; + + @override + String get pageTitleReviewVideo => 'Review Video'; + + @override + String get pageTitleReviewImagePost => 'Review Image Post'; + + @override + String get pageTitleLegal => 'Legal'; + + @override + String get pageTitleFollowers => 'Followers'; + + @override + String get pageTitleFollowing => 'Following'; + + @override + String get emptyNoVideosUsingSound => 'No videos using this sound yet'; + + @override + String get emptyNoPhotoLibrary => + 'No photos or videos found in your library.'; + + @override + String get messagePermissionPhotoLibrary => + 'Allow photo library access to pick photos and videos.'; + + @override + String get messagePostingStory => 'Posting story...'; + + @override + String get messageProcessingVideo => 'Processing video...'; + + @override + String get messageUploadingVideo => 'Uploading video'; + + @override + String get messageReadyToPost => 'Ready to post'; + + @override + String get messageUploadFailed => 'Upload failed'; + + @override + String messageUploadingPercent(int percent) { + return 'Uploading $percent%'; + } + + @override + String get labelOriginalSound => 'Original Sound'; + + @override + String get labelShare => 'Share'; + + @override + String get labelFollowing => 'Following'; + + @override + String get labelPosts => 'Posts'; + + @override + String get labelPrivacyPolicy => 'Privacy Policy'; + + @override + String get labelTermsOfService => 'Terms of Service'; + + @override + String get labelSupport => 'Support'; + + @override + String get tooltipBack => 'Back'; + + @override + String get errorFailedToLoadImage => 'Failed to load image'; + + @override + String get pageTitleSignIn => 'Sign In'; + + @override + String get messageEnterHandle => 'Enter your handle to continue with OAuth'; + + @override + String get messageCompletingSignUp => 'Completing sign up...'; + + @override + String get messageWelcome => 'Welcome!'; + + @override + String get messageWelcomeDescription => + 'Share videos, connect with friends,\nand take back your timeline.'; + + @override + String get labelReply => 'Reply'; + + @override + String get hintAddImage => 'Add image (1 max)'; + + @override + String get messagePostingImage => 'Posting...'; + + @override + String get messageMaximumImagesReached => 'Maximum images reached'; + + @override + String get labelSound => 'Sound'; + + @override + String get labelStickers => 'Stickers'; + + @override + String get labelPaint => 'Paint'; + + @override + String get labelText => 'Text'; + + @override + String get labelCrop => 'Crop'; + + @override + String get labelTune => 'Tune'; + + @override + String get labelFilter => 'Filter'; + + @override + String get labelBlur => 'Blur'; + + @override + String get labelEmoji => 'Emoji'; + + @override + String get labelMention => 'Mention'; + + @override + String get labelDraw => 'Draw'; + + @override + String get labelMyAwesomeVideo => 'My awesome video'; + + @override + String get errorUploadingVideo => 'Uploading video'; + + @override + String get errorProcessingVideoStatus => 'Processing video'; + + @override + String get errorReadyToPost => 'Ready to post'; + + @override + String get errorUploadFailed => 'Upload failed'; + + @override + String errorSnackBar(String error) { + return 'Error: $error'; + } + + @override + String get buttonLikeFeed => 'Like Feed'; + + @override + String get buttonUnlikeFeed => 'Unlike Feed'; + + @override + String get emptyNoNotifications => 'No notifications'; + + @override + String get messageAllCaughtUp => 'You\'re all caught up!'; + + @override + String get messageLabelerConfigDescription => + 'Configure how this labeler\'s content labels are handled in your feeds.'; + + @override + String get errorLoadingNotifications => 'Failed to load notifications'; + + @override + String get labelContentLabelSettings => 'Content Label Settings'; + + @override + String get errorPhotoSelectLimit => + 'You can only select photos in multi-select mode.'; + + @override + String errorPhotoSelectMax(int max) { + return 'You can select up to $max.'; + } + + @override + String get errorUnableToAccessPhotos => + 'Unable to access the selected photos.'; + + @override + String get errorUnableToAccessMedia => 'Unable to access this media item.'; + + @override + String get labelSingleSelect => 'Single Select'; + + @override + String get labelSelectMultiple => 'Select multiple'; + + @override + String get labelLibrary => 'Library'; + + @override + String labelDoneCount(int current, int max) { + return 'Done ($current/$max)'; + } + + @override + String get messageLimitedLibraryAccess => + 'Limited library access is enabled. You can change this in settings.'; + + @override + String get tabPosts => 'Posts'; + + @override + String get tabUsers => 'Users'; + + @override + String get hintSearchByHandle => 'Search by handle or display name'; + + @override + String labelFeedByCreator(String handle) { + return 'by @$handle'; + } + + @override + String get messagePostsFromFollowing => 'Posts from people you follow'; + + @override + String get buttonAddFeed => 'Add feed'; + + @override + String get buttonUnpinFeed => 'Unpin feed'; + + @override + String get buttonPinFeed => 'Pin feed'; + + @override + String get emptyNoConversations => 'No conversations yet'; + + @override + String get messageSending => 'Sending...'; + + @override + String get buttonSend => 'Send'; } diff --git a/lib/src/core/l10n/intl_en.arb b/lib/src/core/l10n/intl_en.arb --- a/lib/src/core/l10n/intl_en.arb +++ b/lib/src/core/l10n/intl_en.arb @@ -287,7 +287,7 @@ "description": "Type message input placeholder" }, - "hintAddDescription": "Add a description... (optional)", + "hintAddDescription": "Add a caption... (optional)", "@hintAddDescription": { "description": "Add description placeholder" }, @@ -485,5 +485,593 @@ "categoryOther": "Other", "@categoryOther": { "description": "Report category: Other" + }, + + "buttonContinue": "Continue", + "@buttonContinue": { + "description": "Continue button text" + }, + + "buttonGetStarted": "Get Started", + "@buttonGetStarted": { + "description": "Get started button text" + }, + + "buttonHaveAccount": "I already have an account", + "@buttonHaveAccount": { + "description": "Already have an account button text" + }, + + "buttonOpen": "Open", + "@buttonOpen": { + "description": "Open button text" + }, + + "buttonPost": "Post", + "@buttonPost": { + "description": "Post button text" + }, + + "buttonDone": "Done", + "@buttonDone": { + "description": "Done button text" + }, + + "buttonEdit": "Edit", + "@buttonEdit": { + "description": "Edit button text" + }, + + "buttonBack": "Back", + "@buttonBack": { + "description": "Back button text" + }, + + "buttonShare": "Share", + "@buttonShare": { + "description": "Share button text" + }, + + "buttonCopied": "Copied", + "@buttonCopied": { + "description": "Copied state text for copy link" + }, + + "buttonCopyLink": "Copy link", + "@buttonCopyLink": { + "description": "Copy link button text" + }, + + "errorInvalidHandle": "Invalid handle", + "@errorInvalidHandle": { + "description": "Error message for invalid handle" + }, + + "errorHandleNotFound": "Could not find this handle", + "@errorHandleNotFound": { + "description": "Error message for handle not found" + }, + + "errorCompletingSignIn": "Completing sign in...", + "@errorCompletingSignIn": { + "description": "Loading message when completing sign in" + }, + + "errorProfileNotFound": "Profile not found", + "@errorProfileNotFound": { + "description": "Error message for profile not found" + }, + + "errorLoadingPost": "Error loading post", + "@errorLoadingPost": { + "description": "Error loading post message" + }, + + "errorLoadingMessages": "Failed to load messages", + "@errorLoadingMessages": { + "description": "Error loading messages message" + }, + + "errorLoadingConversations": "Failed to load conversations", + "@errorLoadingConversations": { + "description": "Error loading conversations message" + }, + + "errorUnableToOpenLink": "Unable to open link right now.", + "@errorUnableToOpenLink": { + "description": "Error message when unable to open a link" + }, + + "errorLoadingLabelerSettings": "Error Loading Labeler Settings", + "@errorLoadingLabelerSettings": { + "description": "Error loading labeler settings message" + }, + + "errorWithDetail": "Error: {error}", + "@errorWithDetail": { + "description": "Error message with detail", + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "hintAddComment": "Add a comment...", + "@hintAddComment": { + "description": "Add a comment input placeholder" + }, + + "hintSearchUsersMessages": "Search users", + "@hintSearchUsersMessages": { + "description": "Search users placeholder in messages" + }, + + "dialogDeleteCommentConfirm": "Are you sure you want to delete this comment? This action cannot be undone.", + "@dialogDeleteCommentConfirm": { + "description": "Delete comment confirmation message" + }, + + "dialogDeletePostConfirm": "Are you sure you want to delete this post? This action cannot be undone.", + "@dialogDeletePostConfirm": { + "description": "Delete post confirmation message" + }, + + "dialogBlockUser": "Block User", + "@dialogBlockUser": { + "description": "Block user dialog title" + }, + + "dialogBlockUserConfirm": "Are you sure you want to block this user? You will no longer see their posts.", + "@dialogBlockUserConfirm": { + "description": "Block user confirmation message" + }, + + "dialogUnblockUser": "Unblock User", + "@dialogUnblockUser": { + "description": "Unblock user dialog title" + }, + + "dialogUnblockUserConfirm": "Are you sure you want to unblock this user?", + "@dialogUnblockUserConfirm": { + "description": "Unblock user confirmation message" + }, + +"dialogRemoveFeedConfirm": "Are you sure you want to remove \"{name}\"?", + "@dialogRemoveFeedConfirm": { + "description": "Remove feed confirmation message with name", + "placeholders": { + "name": { + "type": "String" + } + } + }, + + "dialogOpenBlueskyAccount": "Open Bluesky account management?", + "@dialogOpenBlueskyAccount": { + "description": "Dialog title for opening Bluesky account management" + }, + + "dialogOpenBlueskyAccountDescription": "This opens the Bluesky account management screen. You may have to log in again.\n\nIf prompted for an account provider, use:\n{pdsUrl}", + "@dialogOpenBlueskyAccountDescription": { + "description": "Description for opening Bluesky account management dialog", + "placeholders": { + "pdsUrl": { + "type": "String" + } + } + }, + + "pageTitleReplies": "Replies", + "@pageTitleReplies": { + "description": "Replies page title" + }, + + "pageTitleReviewVideo": "Review Video", + "@pageTitleReviewVideo": { + "description": "Review video page title" + }, + + "pageTitleReviewImagePost": "Review Image Post", + "@pageTitleReviewImagePost": { + "description": "Review image post page title" + }, + + "pageTitleLegal": "Legal", + "@pageTitleLegal": { + "description": "Legal page title" + }, + + "pageTitleFollowers": "Followers", + "@pageTitleFollowers": { + "description": "Followers page title" + }, + + "pageTitleFollowing": "Following", + "@pageTitleFollowing": { + "description": "Following page title" + }, + + "emptyNoVideosUsingSound": "No videos using this sound yet", + "@emptyNoVideosUsingSound": { + "description": "Empty state for no videos using a sound" + }, + + "emptyNoPhotoLibrary": "No photos or videos found in your library.", + "@emptyNoPhotoLibrary": { + "description": "Empty state for no photos in library" + }, + + "messagePermissionPhotoLibrary": "Allow photo library access to pick photos and videos.", + "@messagePermissionPhotoLibrary": { + "description": "Permission message for photo library access" + }, + + "messagePostingStory": "Posting story...", + "@messagePostingStory": { + "description": "Posting story progress message" + }, + + "messageProcessingVideo": "Processing video...", + "@messageProcessingVideo": { + "description": "Processing video progress message" + }, + + "messageUploadingVideo": "Uploading video", + "@messageUploadingVideo": { + "description": "Uploading video status message" + }, + + "messageReadyToPost": "Ready to post", + "@messageReadyToPost": { + "description": "Ready to post status message" + }, + + "messageUploadFailed": "Upload failed", + "@messageUploadFailed": { + "description": "Upload failed status message" + }, + + "messageUploadingPercent": "Uploading {percent}%", + "@messageUploadingPercent": { + "description": "Uploading percentage status message", + "placeholders": { + "percent": { + "type": "int" + } + } + }, + + "labelOriginalSound": "Original Sound", + "@labelOriginalSound": { + "description": "Original sound label" + }, + + "labelShare": "Share", + "@labelShare": { + "description": "Share label" + }, + + "labelFollowing": "Following", + "@labelFollowing": { + "description": "Following label" + }, + + "labelPosts": "Posts", + "@labelPosts": { + "description": "Posts label" + }, + + "labelPrivacyPolicy": "Privacy Policy", + "@labelPrivacyPolicy": { + "description": "Privacy policy link label" + }, + + "labelTermsOfService": "Terms of Service", + "@labelTermsOfService": { + "description": "Terms of service link label" + }, + + "labelSupport": "Support", + "@labelSupport": { + "description": "Support link label" + }, + + "tooltipBack": "Back", + "@tooltipBack": { + "description": "Back tooltip" + }, + + "errorFailedToLoadImage": "Failed to load image", + "@errorFailedToLoadImage": { + "description": "Error message for failed image load" + }, + + "pageTitleSignIn": "Sign In", + "@pageTitleSignIn": { + "description": "Sign in page title" + }, + + "messageEnterHandle": "Enter your handle to continue with OAuth", + "@messageEnterHandle": { + "description": "Message to enter handle for OAuth" + }, + + "messageCompletingSignUp": "Completing sign up...", + "@messageCompletingSignUp": { + "description": "Loading message when completing sign up" + }, + + "messageWelcome": "Welcome!", + "@messageWelcome": { + "description": "Welcome message on register page" + }, + + "messageWelcomeDescription": "Share videos, connect with friends,\nand take back your timeline.", + "@messageWelcomeDescription": { + "description": "Welcome description on register page" + }, + + "labelReply": "Reply", + "@labelReply": { + "description": "Reply button label" + }, + + "hintAddImage": "Add image (1 max)", + "@hintAddImage": { + "description": "Add image tooltip" + }, + + "messagePostingImage": "Posting...", + "@messagePostingImage": { + "description": "Posting image progress message" + }, + + "messageMaximumImagesReached": "Maximum images reached", + "@messageMaximumImagesReached": { + "description": "Maximum images reached tooltip" + }, + + "labelSound": "Sound", + "@labelSound": { + "description": "Sound label for video editor toolbar" + }, + + "labelStickers": "Stickers", + "@labelStickers": { + "description": "Stickers label" + }, + + "labelPaint": "Paint", + "@labelPaint": { + "description": "Paint editor label" + }, + + "labelText": "Text", + "@labelText": { + "description": "Text editor label" + }, + + "labelCrop": "Crop", + "@labelCrop": { + "description": "Crop editor label" + }, + + "labelTune": "Tune", + "@labelTune": { + "description": "Tune editor label" + }, + + "labelFilter": "Filter", + "@labelFilter": { + "description": "Filter editor label" + }, + + "labelBlur": "Blur", + "@labelBlur": { + "description": "Blur editor label" + }, + + "labelEmoji": "Emoji", + "@labelEmoji": { + "description": "Emoji editor label" + }, + + "labelMention": "Mention", + "@labelMention": { + "description": "Mention editor label" + }, + + "labelDraw": "Draw", + "@labelDraw": { + "description": "Draw editor label" + }, + + "labelMyAwesomeVideo": "My awesome video", + "@labelMyAwesomeVideo": { + "description": "Default video clip title" + }, + + "errorUploadingVideo": "Uploading video", + "@errorUploadingVideo": { + "description": "Video upload status when uploading" + }, + + "errorProcessingVideoStatus": "Processing video", + "@errorProcessingVideoStatus": { + "description": "Video processing status" + }, + + "errorReadyToPost": "Ready to post", + "@errorReadyToPost": { + "description": "Ready to post status" + }, + + "errorUploadFailed": "Upload failed", + "@errorUploadFailed": { + "description": "Upload failed status" + }, + + "errorSnackBar": "Error: {error}", + "@errorSnackBar": { + "description": "Error message in snackbar", + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "buttonLikeFeed": "Like Feed", + "@buttonLikeFeed": { + "description": "Like feed button text" + }, + + "buttonUnlikeFeed": "Unlike Feed", + "@buttonUnlikeFeed": { + "description": "Unlike feed button text" + }, + + "emptyNoNotifications": "No notifications", + "@emptyNoNotifications": { + "description": "Empty state for no notifications" + }, + + "messageAllCaughtUp": "You''re all caught up!", + "@messageAllCaughtUp": { + "description": "Message when all notifications are read" + }, + + "messageLabelerConfigDescription": "Configure how this labeler''s content labels are handled in your feeds.", + "@messageLabelerConfigDescription": { + "description": "Description for labeler configuration" + }, + + "errorLoadingNotifications": "Failed to load notifications", + "@errorLoadingNotifications": { + "description": "Error loading notifications message" + }, + + "labelContentLabelSettings": "Content Label Settings", + "@labelContentLabelSettings": { + "description": "Content label settings title" + }, + + "errorPhotoSelectLimit": "You can only select photos in multi-select mode.", + "@errorPhotoSelectLimit": { + "description": "Error when trying to select photos in single-select mode" + }, + + "errorPhotoSelectMax": "You can select up to {max}.", + "@errorPhotoSelectMax": { + "description": "Error when exceeding max photo selection", + "placeholders": { + "max": { + "type": "int" + } + } + }, + + "errorUnableToAccessPhotos": "Unable to access the selected photos.", + "@errorUnableToAccessPhotos": { + "description": "Error when unable to access selected photos" + }, + + "errorUnableToAccessMedia": "Unable to access this media item.", + "@errorUnableToAccessMedia": { + "description": "Error when unable to access a media item" + }, + + "labelSingleSelect": "Single Select", + "@labelSingleSelect": { + "description": "Single select mode label" + }, + + "labelSelectMultiple": "Select multiple", + "@labelSelectMultiple": { + "description": "Select multiple mode label" + }, + + "labelLibrary": "Library", + "@labelLibrary": { + "description": "Library header label" + }, + + "labelDoneCount": "Done ({current}/{max})", + "@labelDoneCount": { + "description": "Done button with selection count", + "placeholders": { + "current": { + "type": "int" + }, + "max": { + "type": "int" + } + } + }, + + "messageLimitedLibraryAccess": "Limited library access is enabled. You can change this in settings.", + "@messageLimitedLibraryAccess": { + "description": "Permission info about limited library access" + }, + + "tabPosts": "Posts", + "@tabPosts": { + "description": "Posts search tab label" + }, + + "tabUsers": "Users", + "@tabUsers": { + "description": "Users search tab label" + }, + + "hintSearchByHandle": "Search by handle or display name", + "@hintSearchByHandle": { + "description": "Search by handle or display name placeholder" + }, + + "labelFeedByCreator": "by @{handle}", + "@labelFeedByCreator": { + "description": "Feed creator attribution label", + "placeholders": { + "handle": { + "type": "String" + } + } + }, + + "messagePostsFromFollowing": "Posts from people you follow", + "@messagePostsFromFollowing": { + "description": "Subtitle for following/timeline feed" + }, + + "buttonAddFeed": "Add feed", + "@buttonAddFeed": { + "description": "Add feed button text" + }, + + "buttonUnpinFeed": "Unpin feed", + "@buttonUnpinFeed": { + "description": "Unpin feed button text" + }, + + "buttonPinFeed": "Pin feed", + "@buttonPinFeed": { + "description": "Pin feed button text" + }, + + "emptyNoConversations": "No conversations yet", + "@emptyNoConversations": { + "description": "Empty state for no conversations in share panel" + }, + + "messageSending": "Sending...", + "@messageSending": { + "description": "Sending message progress indicator" + }, + + "buttonSend": "Send", + "@buttonSend": { + "description": "Send button text" } } diff --git a/lib/src/core/design_system/templates/chat_thread_page_template.dart b/lib/src/core/design_system/templates/chat_thread_page_template.dart --- a/lib/src/core/design_system/templates/chat_thread_page_template.dart +++ b/lib/src/core/design_system/templates/chat_thread_page_template.dart @@ -3,6 +3,7 @@ import 'package:spark/src/core/design_system/components/molecules/input_field.dart'; import 'package:spark/src/core/design_system/components/molecules/profile_avatar.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; class ChatThreadPageTemplate extends StatelessWidget { const ChatThreadPageTemplate({ @@ -25,6 +26,7 @@ @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l10n = AppLocalizations.of(context); return Scaffold( backgroundColor: theme.colorScheme.surface, @@ -77,7 +79,7 @@ padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), child: InputField.chat( controller: textController, - hintText: 'Message...', + hintText: l10n.hintMessage, onSendMessage: onSend, ), ), diff --git a/lib/src/core/design_system/templates/image_review_page_template.dart b/lib/src/core/design_system/templates/image_review_page_template.dart --- a/lib/src/core/design_system/templates/image_review_page_template.dart +++ b/lib/src/core/design_system/templates/image_review_page_template.dart @@ -8,6 +8,7 @@ import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/design_system/tokens/shapes.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/features/posting/models/mention_controller.dart'; import 'package:spark/src/features/posting/ui/widgets/mention_input_field.dart'; @@ -73,6 +74,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final colorScheme = theme.colorScheme; @@ -83,7 +85,7 @@ elevation: 0, leading: AppLeadingButton( color: theme.textTheme.titleLarge?.color, - tooltip: 'Back', + tooltip: l10n.buttonBack, ), title: Text(title), centerTitle: false, @@ -368,6 +370,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final textController = mentionController?.textController ?? controller; final count = textController?.text.runes.length ?? 0; final showCounter = count >= (maxChars * 0.8); @@ -381,11 +384,12 @@ MentionInputField( controller: mentionController!, onMentionsChanged: onMentionsChanged ?? (_) {}, + hintText: l10n.hintAddDescription, ) else if (controller != null) InputField.search( controller: controller!, - hintText: 'Add a description... (optional)', + hintText: l10n.hintAddDescription, maxLines: 5, minLines: 1, ), diff --git a/lib/src/core/design_system/templates/video_review_page_template.dart b/lib/src/core/design_system/templates/video_review_page_template.dart --- a/lib/src/core/design_system/templates/video_review_page_template.dart +++ b/lib/src/core/design_system/templates/video_review_page_template.dart @@ -5,6 +5,7 @@ import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/design_system/tokens/shapes.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/features/posting/models/mention_controller.dart'; import 'package:spark/src/features/posting/ui/widgets/mention_input_field.dart'; @@ -65,6 +66,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final colorScheme = theme.colorScheme; @@ -75,7 +77,7 @@ elevation: 0, leading: AppLeadingButton( color: theme.textTheme.titleLarge?.color, - tooltip: 'Back', + tooltip: l10n.buttonBack, ), title: Text(title), centerTitle: false, @@ -226,7 +228,7 @@ alignment: Alignment.centerRight, child: TextButton( onPressed: onRetry, - child: const Text('Try again'), + child: Text(AppLocalizations.of(context).buttonTryAgain), ), ), ], @@ -317,6 +319,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final textController = mentionController?.textController ?? controller; final count = textController?.text.runes.length ?? 0; final showCounter = count >= (maxChars * 0.8); @@ -330,11 +333,12 @@ MentionInputField( controller: mentionController!, onMentionsChanged: onMentionsChanged ?? (_) {}, + hintText: l10n.hintAddDescription, ) else if (controller != null) InputField.search( controller: controller!, - hintText: 'Add a description... (optional)', + hintText: l10n.hintAddDescription, maxLines: 5, minLines: 1, ), diff --git a/lib/src/core/pro_image_editor/ui/story_image_editor_page.dart b/lib/src/core/pro_image_editor/ui/story_image_editor_page.dart --- a/lib/src/core/pro_image_editor/ui/story_image_editor_page.dart +++ b/lib/src/core/pro_image_editor/ui/story_image_editor_page.dart @@ -7,6 +7,7 @@ import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:pro_image_editor/pro_image_editor.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/pro_image_editor/models/story_image_editor_result.dart'; import 'package:spark/src/core/pro_image_editor/story_mention_editing.dart'; import 'package:spark/src/core/pro_image_editor/story_image_editor_configs.dart'; @@ -171,14 +172,14 @@ children: [ const Icon(Icons.error_outline, color: Colors.red, size: 48), const SizedBox(height: 16), - const Text( - 'Failed to load image', - style: TextStyle(color: Colors.white, fontSize: 18), + Text( + AppLocalizations.of(context).errorFailedToLoadImage, + style: const TextStyle(color: Colors.white, fontSize: 18), ), const SizedBox(height: 8), TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('Go Back'), + child: Text(AppLocalizations.of(context).buttonGoBack), ), ], ), diff --git a/lib/src/core/ui/widgets/alt_text_editor_dialog.dart b/lib/src/core/ui/widgets/alt_text_editor_dialog.dart --- a/lib/src/core/ui/widgets/alt_text_editor_dialog.dart +++ b/lib/src/core/ui/widgets/alt_text_editor_dialog.dart @@ -35,6 +35,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final isDarkMode = theme.brightness == Brightness.dark; final backgroundColor = isDarkMode ? AppColors.nearBlack : Colors.white; @@ -81,7 +82,7 @@ maxLines: 4, style: TextStyle(color: textColor, fontSize: 16), decoration: InputDecoration( - hintText: 'Add alt text', + hintText: l10n.hintAddAltText, hintStyle: TextStyle(color: textColor.withAlpha(100)), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( diff --git a/lib/src/core/ui/widgets/custom_text_field.dart b/lib/src/core/ui/widgets/custom_text_field.dart --- a/lib/src/core/ui/widgets/custom_text_field.dart +++ b/lib/src/core/ui/widgets/custom_text_field.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; /// A custom styled text field with optional undo functionality. class CustomTextField extends StatelessWidget { @@ -55,7 +56,7 @@ ? IconButton( icon: const Icon(Icons.undo, size: 20), onPressed: onUndo, - tooltip: 'Revert', + tooltip: AppLocalizations.of(context).tooltipRevert, ) : null, ), diff --git a/lib/src/core/ui/widgets/report_dialog.dart b/lib/src/core/ui/widgets/report_dialog.dart --- a/lib/src/core/ui/widgets/report_dialog.dart +++ b/lib/src/core/ui/widgets/report_dialog.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/design_system/components/atoms/buttons/long_button.dart'; import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; import 'package:spark/src/core/utils/logging/log_service.dart'; @@ -428,6 +429,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final textColor = theme.textTheme.bodyMedium?.color ?? @@ -507,7 +509,7 @@ maxLines: 3, style: theme.textTheme.bodySmall?.copyWith(color: textColor), decoration: InputDecoration( - hintText: 'Additional details (optional)', + hintText: l10n.hintAdditionalDetails, contentPadding: const EdgeInsets.symmetric( horizontal: 8, vertical: 8, @@ -552,7 +554,7 @@ height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) - : LongButton(label: 'Submit', onPressed: _submitReport), + : LongButton(label: l10n.buttonSubmit, onPressed: _submitReport), ], ); } diff --git a/lib/src/features/posting/utils/story_direct_post.dart b/lib/src/features/posting/utils/story_direct_post.dart --- a/lib/src/features/posting/utils/story_direct_post.dart +++ b/lib/src/features/posting/utils/story_direct_post.dart @@ -4,6 +4,7 @@ import 'package:get_it/get_it.dart'; import 'package:image_picker/image_picker.dart'; import 'package:spark/src/core/design_system/tokens/colors.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/models.dart'; import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; import 'package:spark/src/features/posting/providers/post_story.dart'; @@ -29,7 +30,9 @@ showDialog( context: context, barrierDismissible: false, - builder: (_) => const _PostingOverlay(message: 'Posting story...'), + builder: (_) => _PostingOverlay( + message: AppLocalizations.of(context).messagePostingStory, + ), ); try { @@ -91,7 +94,9 @@ showDialog( context: context, barrierDismissible: false, - builder: (_) => const _PostingOverlay(message: 'Processing video...'), + builder: (_) => _PostingOverlay( + message: AppLocalizations.of(context).messageProcessingVideo, + ), ); try { diff --git a/lib/src/core/design_system/components/molecules/feed_card.dart b/lib/src/core/design_system/components/molecules/feed_card.dart --- a/lib/src/core/design_system/components/molecules/feed_card.dart +++ b/lib/src/core/design_system/components/molecules/feed_card.dart @@ -6,6 +6,7 @@ import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/design_system/tokens/shapes.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; class FeedCard extends StatelessWidget { @@ -34,19 +35,19 @@ bool get _isTimeline => feed.type == 'timeline'; - String get _title { + String _getTitle(AppLocalizations l10n) { if (generator != null) { return generator!.displayName; } - return _isTimeline ? 'Following' : feed.config.value; + return _isTimeline ? l10n.labelFollowing : feed.config.value; } - String? get _subtitle { + String? _getSubtitle(AppLocalizations l10n) { if (generator != null) { - return 'by @${generator!.creator.handle}'; + return l10n.labelFeedByCreator(generator!.creator.handle); } if (_isTimeline) { - return 'Posts from people you follow'; + return l10n.messagePostsFromFollowing; } return null; } @@ -62,10 +63,10 @@ bool get _isPrimaryAction => !isAdded || !isPinned; - String get _actionLabel { - if (!isAdded) return 'Add feed'; - if (isPinned) return 'Unpin feed'; - return 'Pin feed'; + String _getActionLabel(AppLocalizations l10n) { + if (!isAdded) return l10n.buttonAddFeed; + if (isPinned) return l10n.buttonUnpinFeed; + return l10n.buttonPinFeed; } VoidCallback get _actionCallback { @@ -127,18 +128,20 @@ } Widget _buildTextContent(BuildContext context) { + final l10n = AppLocalizations.of(context); + final subtitle = _getSubtitle(l10n); return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - _title, + _getTitle(l10n), style: AppTypography.textSmallBold, overflow: TextOverflow.ellipsis, ), - if (_subtitle != null) + if (subtitle != null) Text( - _subtitle!, + subtitle, style: AppTypography.textSmallThin, overflow: TextOverflow.ellipsis, ), @@ -180,6 +183,7 @@ } Widget _buildActionButton(BuildContext context) { + final l10n = AppLocalizations.of(context); final isDark = Theme.of(context).brightness == Brightness.dark; final isPrimary = _isPrimaryAction; final backgroundColor = isPrimary @@ -218,7 +222,7 @@ ), alignment: Alignment.center, child: Text( - _actionLabel, + _getActionLabel(l10n), style: textStyle, overflow: TextOverflow.ellipsis, ), diff --git a/lib/src/core/design_system/components/molecules/profile_action_buttons.dart b/lib/src/core/design_system/components/molecules/profile_action_buttons.dart --- a/lib/src/core/design_system/components/molecules/profile_action_buttons.dart +++ b/lib/src/core/design_system/components/molecules/profile_action_buttons.dart @@ -3,6 +3,7 @@ import 'package:spark/src/core/design_system/components/atoms/toggles/follow_button.dart'; import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; class ProfileActionButtons extends StatelessWidget { const ProfileActionButtons({ @@ -26,6 +27,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); if (isCurrentUser) { return Row( children: [Expanded(child: _EditButton(onTap: onEditTap))], @@ -41,7 +43,7 @@ onFollow: onFollowTap ?? () {}, onUnfollow: onUnfollowTap ?? () {}, onUnblock: onUnblockTap, - unfollowText: 'Following', + unfollowText: l10n.labelFollowing, width: double.infinity, ), ), diff --git a/lib/src/core/design_system/components/molecules/profile_stats.dart b/lib/src/core/design_system/components/molecules/profile_stats.dart --- a/lib/src/core/design_system/components/molecules/profile_stats.dart +++ b/lib/src/core/design_system/components/molecules/profile_stats.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; class ProfileStats extends StatelessWidget { const ProfileStats({ @@ -20,19 +21,23 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _StatItem(count: postsCount, label: 'Posts'), + _StatItem(count: postsCount, label: l10n.labelPosts), GestureDetector( onTap: onFollowersTap, behavior: HitTestBehavior.opaque, - child: _StatItem(count: followersCount, label: 'Followers'), + child: _StatItem( + count: followersCount, + label: l10n.pageTitleFollowers, + ), ), GestureDetector( onTap: onFollowingTap, behavior: HitTestBehavior.opaque, - child: _StatItem(count: followingCount, label: 'Following'), + child: _StatItem(count: followingCount, label: l10n.labelFollowing), ), ], ); diff --git a/lib/src/core/design_system/components/molecules/settings_feed_card.dart b/lib/src/core/design_system/components/molecules/settings_feed_card.dart --- a/lib/src/core/design_system/components/molecules/settings_feed_card.dart +++ b/lib/src/core/design_system/components/molecules/settings_feed_card.dart @@ -5,6 +5,7 @@ import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/design_system/tokens/shapes.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; enum SettingsFeedCardMode { display, edit } @@ -41,11 +42,11 @@ bool get _isLiked => feed.view?.viewer?.like != null; - String get _title { + String _getTitle(AppLocalizations l10n) { if (generator != null) { return generator!.displayName; } - return _isTimeline ? 'Following' : feed.config.value; + return _isTimeline ? l10n.labelFollowing : feed.config.value; } String? get _subtitle { @@ -101,7 +102,7 @@ children: [ _buildAvatar(), const SizedBox(width: 12), - Expanded(child: _buildTextContent()), + Expanded(child: _buildTextContent(context)), const SizedBox(width: 8), _buildLikeButton(), ], @@ -134,7 +135,7 @@ children: [ _buildAvatar(), const SizedBox(width: 12), - Expanded(child: _buildTextContent()), + Expanded(child: _buildTextContent(context)), const SizedBox(width: 8), _buildEditActions(), const SizedBox(width: 8), @@ -185,13 +186,14 @@ ); } - Widget _buildTextContent() { + Widget _buildTextContent(BuildContext context) { + final l10n = AppLocalizations.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( - _title, + _getTitle(l10n), style: AppTypography.textSmallBold, overflow: TextOverflow.ellipsis, maxLines: 1, diff --git a/lib/src/core/pro_image_editor/ui/widgets/story_editor_toolbar.dart b/lib/src/core/pro_image_editor/ui/widgets/story_editor_toolbar.dart --- a/lib/src/core/pro_image_editor/ui/widgets/story_editor_toolbar.dart +++ b/lib/src/core/pro_image_editor/ui/widgets/story_editor_toolbar.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:spark/src/core/design_system/tokens/colors.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; /// Toolbar widget for the Story Image Editor. /// @@ -26,33 +27,42 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final items = [ if (onMention != null) _ToolbarItem( icon: Icons.alternate_email_rounded, - label: 'Mention', + label: l10n.labelMention, onTap: () => onMention!.call(), ), - _ToolbarItem(icon: Icons.brush_rounded, label: 'Draw', onTap: onPaint), + _ToolbarItem( + icon: Icons.brush_rounded, + label: l10n.labelDraw, + onTap: onPaint, + ), _ToolbarItem( icon: Icons.text_fields_rounded, - label: 'Text', + label: l10n.labelText, onTap: onText, ), _ToolbarItem( icon: Icons.auto_awesome_rounded, - label: 'Filter', + label: l10n.labelFilter, onTap: onFilter, ), - _ToolbarItem(icon: Icons.blur_on_rounded, label: 'Blur', onTap: onBlur), + _ToolbarItem( + icon: Icons.blur_on_rounded, + label: l10n.labelBlur, + onTap: onBlur, + ), _ToolbarItem( icon: Icons.emoji_emotions_rounded, - label: 'Emoji', + label: l10n.labelEmoji, onTap: onEmoji, ), _ToolbarItem( icon: Icons.sticky_note_2_rounded, - label: 'Stickers', + label: l10n.labelStickers, onTap: onStickers, ), ]; diff --git a/lib/src/core/pro_image_editor/ui/widgets/story_mention_picker_sheet.dart b/lib/src/core/pro_image_editor/ui/widgets/story_mention_picker_sheet.dart --- a/lib/src/core/pro_image_editor/ui/widgets/story_mention_picker_sheet.dart +++ b/lib/src/core/pro_image_editor/ui/widgets/story_mention_picker_sheet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; import 'package:spark/src/features/search/providers/actor_typeahead_provider.dart'; import 'package:spark/src/features/search/providers/actor_typeahead_state.dart'; @@ -38,6 +39,7 @@ @override Widget build(BuildContext context) { final typeaheadState = ref.watch(actorTypeaheadProvider); + final l10n = AppLocalizations.of(context); return Padding( padding: EdgeInsets.only( @@ -84,7 +86,7 @@ style: const TextStyle(color: Colors.white), cursorColor: Colors.white, decoration: InputDecoration( - hintText: 'Search by handle or display name', + hintText: l10n.hintSearchByHandle, hintStyle: const TextStyle(color: Color(0xFF64748B)), prefixIcon: const Icon(Icons.search, color: Color(0xFF94A3B8)), filled: true, diff --git a/lib/src/features/auth/ui/pages/login_page.dart b/lib/src/features/auth/ui/pages/login_page.dart --- a/lib/src/features/auth/ui/pages/login_page.dart +++ b/lib/src/features/auth/ui/pages/login_page.dart @@ -7,6 +7,7 @@ import 'package:spark/src/core/design_system/components/atoms/buttons/app_overlay_back_button.dart'; import 'package:spark/src/core/design_system/components/atoms/buttons/long_button.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/features/auth/providers/auth_providers.dart'; import 'package:spark/src/features/auth/providers/onboarding_providers.dart'; @@ -132,6 +133,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final isLoading = ref.watch( authProvider.select((state) => state.isLoading), ); @@ -155,7 +157,7 @@ children: [ if (!_hasReceivedCallback) ...[ Text( - 'Sign In', + l10n.pageTitleSignIn, style: AppTypography.displaySmallBold.copyWith( color: colorScheme.onSurface, ), @@ -163,7 +165,7 @@ ), const SizedBox(height: 8), Text( - 'Enter your handle to continue with OAuth', + l10n.messageEnterHandle, style: AppTypography.textMediumMedium.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -231,10 +233,10 @@ switch (error) { final String e when e.contains('must be a valid handle') => - 'Invalid handle', + l10n.errorInvalidHandle, final String e when e.contains('Failed to resolve') => - 'Could not find this handle', + l10n.errorHandleNotFound, _ => error, }, style: AppTypography.textSmallMedium.copyWith( @@ -250,7 +252,7 @@ const CircularProgressIndicator(), const SizedBox(height: 16), Text( - 'Completing sign in...', + l10n.errorCompletingSignIn, style: AppTypography.textMediumMedium.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -262,7 +264,7 @@ Opacity( opacity: isLoading ? 0.5 : 1.0, child: LongButton( - label: 'Continue', + label: l10n.buttonContinue, onPressed: isLoading ? null : _initiateOAuth, ), ), diff --git a/lib/src/features/auth/ui/pages/register_page.dart b/lib/src/features/auth/ui/pages/register_page.dart --- a/lib/src/features/auth/ui/pages/register_page.dart +++ b/lib/src/features/auth/ui/pages/register_page.dart @@ -6,6 +6,7 @@ import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:spark/src/core/design_system/components/atoms/buttons/long_button.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/features/auth/providers/auth_providers.dart'; import 'package:spark/src/features/auth/providers/onboarding_providers.dart'; @@ -111,6 +112,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final isLoading = ref.watch( authProvider.select((state) => state.isLoading), ); @@ -136,7 +138,7 @@ const CircularProgressIndicator(), const SizedBox(height: 16), Text( - 'Completing sign up...', + l10n.messageCompletingSignUp, style: AppTypography.textMediumMedium.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -163,7 +165,7 @@ const SizedBox(height: 32), // Welcome text Text( - 'Welcome!', + l10n.messageWelcome, style: AppTypography.displaySmallBold.copyWith( color: colorScheme.onSurface, ), @@ -171,8 +173,7 @@ ), const SizedBox(height: 12), Text( - 'Share videos, connect with friends,\n' - 'and take back your timeline.', + l10n.messageWelcomeDescription, style: AppTypography.textMediumMedium.copyWith( color: colorScheme.onSurfaceVariant, height: 1.5, @@ -198,13 +199,13 @@ Opacity( opacity: isLoading ? 0.5 : 1.0, child: LongButton( - label: 'Get Started', + label: l10n.buttonGetStarted, onPressed: isLoading ? null : _initiateOAuth, ), ), const SizedBox(height: 12), LongButton( - label: 'I already have an account', + label: l10n.buttonHaveAccount, variant: LongButtonVariant.regular, onPressed: () => context.router.push(const LoginRoute()), ), diff --git a/lib/src/features/comments/ui/pages/crosspost_comments_page.dart b/lib/src/features/comments/ui/pages/crosspost_comments_page.dart --- a/lib/src/features/comments/ui/pages/crosspost_comments_page.dart +++ b/lib/src/features/comments/ui/pages/crosspost_comments_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; import 'package:spark/src/core/ui/widgets/image_content.dart'; @@ -31,6 +32,7 @@ @override Widget build(BuildContext context, WidgetRef ref) { final anchorUri = AtUri.parse(postUri); + final l10n = AppLocalizations.of(context); final asyncComments = ref.watch(crosspostCommentsProvider(anchorUri)); final textColor = Theme.of(context).colorScheme.onSurface; final borderColor = Theme.of(context).colorScheme.outline; @@ -76,9 +78,7 @@ child: asyncComments.when( data: (comments) { if (comments.isEmpty) { - return const Center( - child: Text('No crosspost comments yet.'), - ); + return Center(child: Text(l10n.emptyNoCrosspostComments)); } return ListView.separated( @@ -97,7 +97,7 @@ }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, stackTrace) => - Center(child: Text('Error: $error')), + Center(child: Text(l10n.errorWithDetail(error.toString()))), ), ), ], diff --git a/lib/src/features/comments/ui/pages/replies_page.dart b/lib/src/features/comments/ui/pages/replies_page.dart --- a/lib/src/features/comments/ui/pages/replies_page.dart +++ b/lib/src/features/comments/ui/pages/replies_page.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/design_system/components/atoms/buttons/app_leading_button.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/features/comments/providers/comments_page_provider.dart'; @@ -59,6 +60,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final state = ref.watch( commentsPageProvider(postUri: AtUri.parse(widget.postUri)), ); @@ -72,7 +74,7 @@ backgroundColor: backgroundColor, elevation: 0, title: Text( - 'Replies', + l10n.pageTitleReplies, style: TextStyle(color: textColor, fontWeight: FontWeight.bold), ), leading: AppLeadingButton(color: textColor), @@ -118,7 +120,8 @@ ], ), ), - error: (error, stackTrace) => Center(child: Text('Error: $error')), + error: (error, stackTrace) => + Center(child: Text(l10n.errorWithDetail(error.toString()))), loading: () => const Center(child: CircularProgressIndicator()), ), ); diff --git a/lib/src/features/comments/ui/widgets/comment_input.dart b/lib/src/features/comments/ui/widgets/comment_input.dart --- a/lib/src/features/comments/ui/widgets/comment_input.dart +++ b/lib/src/features/comments/ui/widgets/comment_input.dart @@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:spark/src/core/design_system/tokens/colors.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/design_system/tokens/constants.dart'; import 'package:spark/src/core/design_system/tokens/typography.dart'; import 'package:spark/src/core/ui/widgets/alt_text_editor_dialog.dart'; @@ -48,6 +49,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final state = ref.watch(commentInputProvider(imagePicker)); final notifier = ref.read(commentInputProvider(imagePicker).notifier); final authState = ref.watch(authProvider); @@ -96,7 +98,7 @@ child: MentionInputField( controller: state.mentionController, onMentionsChanged: (_) {}, - hintText: 'Add a comment...', + hintText: l10n.hintAddComment, maxChars: _maxChars, maxLines: 5, minLines: 1, @@ -253,6 +255,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final canAddMoreImages = state.selectedImages.isEmpty; final enabled = !state.isPosting && canAddMoreImages; @@ -262,8 +265,10 @@ constraints: const BoxConstraints(minWidth: 16, minHeight: 16), onPressed: enabled ? () => notifier.pickImages(context) : null, tooltip: enabled - ? 'Add image (1 max)' - : (state.isPosting ? 'Posting...' : 'Maximum images reached'), + ? l10n.hintAddImage + : (state.isPosting + ? l10n.messagePostingStory + : l10n.messageMaximumImagesReached), icon: Icon( FluentIcons.image_24_regular, size: 24, diff --git a/lib/src/features/comments/ui/widgets/comment_item.dart b/lib/src/features/comments/ui/widgets/comment_item.dart --- a/lib/src/features/comments/ui/widgets/comment_item.dart +++ b/lib/src/features/comments/ui/widgets/comment_item.dart @@ -86,10 +86,7 @@ context: context, builder: (context) => AlertDialog( title: Text(l10n.dialogDeleteComment), - content: Text( - 'Are you sure you want to delete this comment? This action ' - 'cannot be undone.', - ), + content: Text(l10n.dialogDeleteCommentConfirm), actions: [ TextButton( onPressed: () => context.router.maybePop(), @@ -330,6 +327,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final notifier = ref.read(commentProvider(widget.thread).notifier); return Row( children: [ @@ -369,7 +367,7 @@ ); }, child: Text( - 'Reply', + l10n.labelReply, style: TextStyle(fontSize: 12, color: secondaryTextColor), ), ), diff --git a/lib/src/features/feed/ui/pages/feed_page.dart b/lib/src/features/feed/ui/pages/feed_page.dart --- a/lib/src/features/feed/ui/pages/feed_page.dart +++ b/lib/src/features/feed/ui/pages/feed_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/ui/foundation/colors.dart'; import 'package:spark/src/features/feed/providers/feed_action_controller.dart'; @@ -87,6 +88,7 @@ Widget build(BuildContext context) { super.build(context); // Required for AutomaticKeepAliveClientMixin + final l10n = AppLocalizations.of(context); final state = ref.watch(feedProvider(widget.feed)); final notifier = ref.read(feedProvider(widget.feed).notifier); final shouldBeActive = ref.watch( @@ -156,10 +158,10 @@ child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text('Error loading feed'), + Text(l10n.errorLoadingFeed), TextButton( onPressed: onRefresh, - child: const Text('Try again'), + child: Text(l10n.buttonTryAgain), ), ], ), diff --git a/lib/src/features/feed/ui/pages/standalone_post_page.dart b/lib/src/features/feed/ui/pages/standalone_post_page.dart --- a/lib/src/features/feed/ui/pages/standalone_post_page.dart +++ b/lib/src/features/feed/ui/pages/standalone_post_page.dart @@ -7,6 +7,7 @@ import 'package:get_it/get_it.dart'; import 'package:spark/src/core/design_system/components/atoms/buttons/app_overlay_back_button.dart'; import 'package:spark/src/core/design_system/tokens/constants.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; import 'package:spark/src/core/routing/app_router.dart'; @@ -140,6 +141,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); // Watch for post updates to trigger reload final updateCount = ref.watch(postUpdateProvider(widget.postUri)); @@ -251,7 +253,7 @@ const Icon(Icons.error, color: Colors.white, size: 48), const SizedBox(height: 16), Text( - 'Error loading post: ${snapshot.error}', + l10n.errorWithDetail(snapshot.error.toString()), style: const TextStyle(color: Colors.white), textAlign: TextAlign.center, ), diff --git a/lib/src/features/messages/ui/pages/chat_page.dart b/lib/src/features/messages/ui/pages/chat_page.dart --- a/lib/src/features/messages/ui/pages/chat_page.dart +++ b/lib/src/features/messages/ui/pages/chat_page.dart @@ -9,6 +9,7 @@ import 'package:spark/src/features/auth/providers/auth_providers.dart'; import 'package:spark/src/features/messages/providers/conversation_provider.dart'; import 'package:spark/src/features/messages/providers/polling_timer.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/features/messages/ui/widgets/messages_list.dart'; @RoutePage() @@ -67,6 +68,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final state = ref.watch(conversationProvider(widget.conversationId)); ref ..listen( @@ -109,14 +111,14 @@ ), const SizedBox(height: 16), Text( - 'Failed to load messages', + l10n.errorLoadingMessages, style: TextStyle(color: Theme.of(context).colorScheme.error), ), const SizedBox(height: 8), ElevatedButton( onPressed: () => ref.invalidate(conversationProvider(widget.conversationId)), - child: const Text('Retry'), + child: Text(l10n.buttonRetry), ), ], ), diff --git a/lib/src/features/messages/ui/pages/messages_page.dart b/lib/src/features/messages/ui/pages/messages_page.dart --- a/lib/src/features/messages/ui/pages/messages_page.dart +++ b/lib/src/features/messages/ui/pages/messages_page.dart @@ -6,6 +6,7 @@ import 'package:spark/src/core/design_system/templates/chat_list_page_template.dart'; import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/core/utils/logging/logging.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/features/messages/providers/conversations_provider.dart'; @RoutePage() @@ -19,6 +20,7 @@ class _MessagesPageState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final logger = GetIt.instance().getLogger('MessagesPage'); final chatServiceState = ref.watch(conversationsProvider); @@ -89,13 +91,13 @@ ), const SizedBox(height: 16), Text( - 'Failed to load conversations', + l10n.errorLoadingConversations, style: TextStyle(color: theme.colorScheme.error), ), const SizedBox(height: 8), ElevatedButton( onPressed: () => ref.invalidate(conversationsProvider), - child: const Text('Retry'), + child: Text(l10n.buttonRetry), ), ], ), diff --git a/lib/src/features/messages/ui/pages/new_chat_search_page.dart b/lib/src/features/messages/ui/pages/new_chat_search_page.dart --- a/lib/src/features/messages/ui/pages/new_chat_search_page.dart +++ b/lib/src/features/messages/ui/pages/new_chat_search_page.dart @@ -8,6 +8,7 @@ import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; import 'package:spark/src/core/network/messages/data/repository/messages_repository.dart'; import 'package:spark/src/core/routing/app_router.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/features/search/providers/search_provider.dart'; @RoutePage() @@ -43,6 +44,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final searchState = ref.watch(searchProvider); final theme = Theme.of(context); final colorScheme = theme.colorScheme; @@ -59,13 +61,13 @@ padding: const EdgeInsets.all(16), child: Row( children: [ - const AppLeadingButton(tooltip: 'Back'), + AppLeadingButton(tooltip: l10n.tooltipBack), const SizedBox(width: 8), Expanded( child: TextField( controller: _searchController, decoration: InputDecoration( - hintText: 'Search users', + hintText: l10n.hintSearchUsers, prefixIcon: const Icon(FluentIcons.search_24_regular), suffixIcon: _searchController.text.isNotEmpty ? IconButton( @@ -131,7 +133,7 @@ ), ), child: TabBar( - tabs: const [Tab(text: 'Users')], + tabs: [Tab(text: l10n.tabUsers)], indicatorColor: colorScheme.primary, labelColor: theme.textTheme.bodyLarge?.color, unselectedLabelColor: theme.textTheme.bodyMedium?.color, diff --git a/lib/src/features/messages/ui/widgets/message_input.dart b/lib/src/features/messages/ui/widgets/message_input.dart --- a/lib/src/features/messages/ui/widgets/message_input.dart +++ b/lib/src/features/messages/ui/widgets/message_input.dart @@ -5,6 +5,7 @@ import 'package:image_picker/image_picker.dart'; import 'package:spark/src/core/ui/foundation/colors.dart'; import 'package:spark/src/core/ui/widgets/user_avatar.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/features/auth/providers/auth_providers.dart'; import 'package:spark/src/features/profile/providers/profile_provider.dart'; @@ -26,6 +27,7 @@ @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context); final authState = ref.watch(authProvider); final userDid = authState.did ?? ''; final userHandle = authState.handle ?? ''; @@ -70,7 +72,7 @@ child: TextField( controller: controller, decoration: InputDecoration( - hintText: 'Type a message...', + hintText: l10n.hintTypeMessage, hintStyle: TextStyle( color: Theme.of(context).colorScheme.onSurface, ), diff --git a/lib/src/features/notifications/ui/widgets/notifications_list.dart b/lib/src/features/notifications/ui/widgets/notifications_list.dart --- a/lib/src/features/notifications/ui/widgets/notifications_list.dart +++ b/lib/src/features/notifications/ui/widgets/notifications_list.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/features/notifications/providers/notification_provider.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/features/notifications/ui/widgets/notification_item.dart'; class NotificationsList extends ConsumerStatefulWidget { @@ -47,6 +48,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final notificationState = ref.watch( notificationProvider(priority: widget.priority, reasons: widget.reasons), ); @@ -89,7 +91,7 @@ ), const SizedBox(height: 16), Text( - 'Failed to load notifications', + l10n.errorLoadingNotifications, style: TextStyle( color: colorScheme.onSurface.withAlpha(179), fontSize: 16, @@ -121,7 +123,7 @@ reasons: widget.reasons, ); }, - child: const Text('Retry'), + child: Text(l10n.buttonRetry), ), ], ), @@ -160,7 +162,7 @@ ), const SizedBox(height: 16), Text( - 'No notifications', + l10n.emptyNoNotifications, style: TextStyle( color: colorScheme.onSurface.withAlpha(179), fontSize: 18, @@ -169,7 +171,7 @@ ), const SizedBox(height: 8), Text( - "You're all caught up!", + l10n.messageAllCaughtUp, style: TextStyle( color: colorScheme.onSurface.withAlpha(102), fontSize: 14, diff --git a/lib/src/features/posting/ui/pages/image_review_page.dart b/lib/src/features/posting/ui/pages/image_review_page.dart --- a/lib/src/features/posting/ui/pages/image_review_page.dart +++ b/lib/src/features/posting/ui/pages/image_review_page.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/design_system/templates/image_review_page_template.dart'; import 'package:spark/src/core/design_system/tokens/constants.dart'; import 'package:spark/src/core/network/atproto/atproto.dart'; @@ -158,13 +159,14 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final canPickMore = _imageFiles.length < _maxImages; final showCrossPostWarning = _crosspostToBsky && _imageFiles.length > 4; final textLength = _descriptionController.text.runes.length; final isOverLimit = textLength > AppConstants.postDescriptionMaxChars; return ImageReviewPageTemplate( - title: 'Review Image Post', + title: l10n.pageTitleReviewImagePost, onBack: () => context.router.maybePop(), imagePaths: _imageFiles.map((e) => e.path).toList(), currentPage: _currentPage, @@ -193,7 +195,7 @@ crossPostValue: _crosspostToBsky, onCrossPostChanged: (v) => setState(() => _crosspostToBsky = v), showCrossPostWarning: showCrossPostWarning, - postLabel: 'Post', + postLabel: l10n.buttonPost, isPosting: _isPosting, isOverLimit: isOverLimit, onPost: _isPosting diff --git a/lib/src/features/posting/ui/pages/media_picker_page.dart b/lib/src/features/posting/ui/pages/media_picker_page.dart --- a/lib/src/features/posting/ui/pages/media_picker_page.dart +++ b/lib/src/features/posting/ui/pages/media_picker_page.dart @@ -4,6 +4,7 @@ import 'package:get_it/get_it.dart'; import 'package:image_picker/image_picker.dart'; import 'package:photo_manager/photo_manager.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/utils/logging/logging.dart'; import 'package:spark/src/features/posting/ui/models/media_selection.dart'; @@ -207,7 +208,7 @@ void _toggleMultiPhotoSelection(AssetEntity asset) { if (asset.type != AssetType.image) { - _showSnackBar('You can only select photos in multi-select mode.'); + _showSnackBar(AppLocalizations.of(context).errorPhotoSelectLimit); return; } @@ -223,7 +224,11 @@ } if (_selectedPhotoAssets.length >= widget.maxMultiPhotoSelection) { - _showSnackBar('You can select up to ${widget.maxMultiPhotoSelection}.'); + _showSnackBar( + AppLocalizations.of( + context, + ).errorPhotoSelectMax(widget.maxMultiPhotoSelection), + ); return; } @@ -235,6 +240,9 @@ Future _submitMultiPhotoSelection() async { if (_selectedPhotoAssets.isEmpty) return; + final errorUnableToAccessPhotos = AppLocalizations.of( + context, + ).errorUnableToAccessPhotos; final files = []; for (final asset in _selectedPhotoAssets) { final file = await _assetToXFile(asset, showErrorMessage: false); @@ -244,7 +252,7 @@ } if (files.isEmpty) { - _showSnackBar('Unable to access the selected photos.'); + _showSnackBar(errorUnableToAccessPhotos); return; } @@ -256,11 +264,15 @@ AssetEntity asset, { required bool showErrorMessage, }) async { + final errorUnableToAccessMedia = showErrorMessage + ? AppLocalizations.of(context).errorUnableToAccessMedia + : null; + try { final file = await asset.file ?? await asset.originFile; if (file == null) { - if (showErrorMessage) { - _showSnackBar('Unable to access this media item.'); + if (errorUnableToAccessMedia != null) { + _showSnackBar(errorUnableToAccessMedia); } return null; } @@ -272,8 +284,8 @@ error: e, stackTrace: stackTrace, ); - if (showErrorMessage) { - _showSnackBar('Unable to access this media item.'); + if (errorUnableToAccessMedia != null) { + _showSnackBar(errorUnableToAccessMedia); } return null; } @@ -308,9 +320,10 @@ Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; + final l10n = AppLocalizations.of(context); final multiSelectLabel = _isMultiPhotoSelection - ? 'Single Select' - : 'Select multiple'; + ? l10n.labelSingleSelect + : l10n.labelSelectMultiple; return ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), @@ -335,9 +348,9 @@ onPressed: () => Navigator.of(context).maybePop(), icon: const Icon(Icons.close), ), - const Expanded( + Expanded( child: Text( - 'Library', + l10n.labelLibrary, textAlign: TextAlign.center, style: TextStyle( fontSize: 18, @@ -372,7 +385,10 @@ ? null : _submitMultiPhotoSelection, child: Text( - 'Done (${_selectedPhotoAssets.length}/${widget.maxMultiPhotoSelection})', + l10n.labelDoneCount( + _selectedPhotoAssets.length, + widget.maxMultiPhotoSelection, + ), ), ), ], @@ -389,8 +405,7 @@ borderRadius: BorderRadius.circular(10), ), child: Text( - 'Limited library access is enabled. ' - 'You can change this in settings.', + l10n.messageLimitedLibraryAccess, style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSecondaryContainer, ), @@ -418,19 +433,19 @@ children: [ const Icon(Icons.photo_library_outlined, size: 40), const SizedBox(height: 12), - const Text( - 'Allow photo library access to pick photos and videos.', + Text( + AppLocalizations.of(context).messagePermissionPhotoLibrary, textAlign: TextAlign.center, ), const SizedBox(height: 16), FilledButton( onPressed: _requestPermissionAndLoadAssets, - child: const Text('Allow Access'), + child: Text(AppLocalizations.of(context).buttonAllowAccess), ), const SizedBox(height: 8), - const TextButton( + TextButton( onPressed: PhotoManager.openSetting, - child: Text('Open Settings'), + child: Text(AppLocalizations.of(context).buttonOpenSettings), ), ], ), @@ -448,8 +463,8 @@ } if (_mediaAssets.isEmpty) { - return const Center( - child: Text('No photos or videos found in your library.'), + return Center( + child: Text(AppLocalizations.of(context).emptyNoPhotoLibrary), ); } diff --git a/lib/src/features/posting/ui/pages/recording_page.dart b/lib/src/features/posting/ui/pages/recording_page.dart --- a/lib/src/features/posting/ui/pages/recording_page.dart +++ b/lib/src/features/posting/ui/pages/recording_page.dart @@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; import 'package:pro_video_editor/pro_video_editor.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/design_system/templates/recording_page_template.dart'; import 'package:spark/src/core/pro_video_editor/models/video_editor_result.dart'; import 'package:spark/src/core/pro_video_editor/pro_video_editor_repository.dart'; @@ -197,9 +198,13 @@ setState(() { _isProcessing = false; }); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Error: $e'))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + AppLocalizations.of(context).errorWithDetail(e.toString()), + ), + ), + ); } } } @@ -274,9 +279,13 @@ setState(() { _isProcessing = false; }); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Error: $e'))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + AppLocalizations.of(context).errorWithDetail(e.toString()), + ), + ), + ); } } } @@ -414,9 +423,13 @@ setState(() { _isProcessing = false; }); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Error: $e'))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + AppLocalizations.of(context).errorWithDetail(e.toString()), + ), + ), + ); } } } @@ -473,7 +486,7 @@ const SizedBox(height: 24), ElevatedButton( onPressed: () => context.router.pop(), - child: const Text('Go Back'), + child: Text(AppLocalizations.of(context).buttonGoBack), ), ], ), @@ -611,7 +624,7 @@ const SizedBox(height: 24), ElevatedButton( onPressed: () => context.router.pop(), - child: const Text('Go Back'), + child: Text(AppLocalizations.of(context).buttonGoBack), ), ], ), diff --git a/lib/src/features/posting/ui/pages/story_post_page.dart b/lib/src/features/posting/ui/pages/story_post_page.dart --- a/lib/src/features/posting/ui/pages/story_post_page.dart +++ b/lib/src/features/posting/ui/pages/story_post_page.dart @@ -8,6 +8,7 @@ import 'package:spark/src/core/network/atproto/data/models/models.dart'; import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; import 'package:spark/src/core/utils/error_messages.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/features/posting/providers/post_story.dart'; import 'package:spark/src/features/posting/providers/video_upload_provider.dart'; @@ -95,7 +96,7 @@ } setState(() { - _statusMessage = 'Posting story...'; + _statusMessage = AppLocalizations.of(context).messagePostingStory; }); final uploadedImage = uploadedImages.first; @@ -113,7 +114,7 @@ Future _postVideoStory() async { setState(() { - _statusMessage = 'Processing video...'; + _statusMessage = AppLocalizations.of(context).messageProcessingVideo; }); final result = await ref.read( @@ -301,7 +302,7 @@ borderRadius: BorderRadius.circular(12), ), ), - child: const Text('Cancel'), + child: Text(AppLocalizations.of(context).buttonCancel), ), ), const SizedBox(width: 10), @@ -316,7 +317,7 @@ borderRadius: BorderRadius.circular(12), ), ), - child: const Text('Retry'), + child: Text(AppLocalizations.of(context).buttonRetry), ), ), ], diff --git a/lib/src/features/posting/ui/pages/video_review_page.dart b/lib/src/features/posting/ui/pages/video_review_page.dart --- a/lib/src/features/posting/ui/pages/video_review_page.dart +++ b/lib/src/features/posting/ui/pages/video_review_page.dart @@ -8,6 +8,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/design_system/templates/video_review_page_template.dart'; import 'package:spark/src/core/design_system/tokens/constants.dart'; import 'package:spark/src/core/network/atproto/atproto.dart'; @@ -220,29 +221,29 @@ }); } - String? get _uploadStatusLabel { + String? _uploadStatusLabel(AppLocalizations l10n) { if (_uploadErrorMessage != null) return _uploadErrorMessage; return switch (_uploadPhase) { - _VideoUploadPhase.uploading => 'Uploading video', - _VideoUploadPhase.processing => 'Processing video', - _VideoUploadPhase.ready => 'Ready to post', + _VideoUploadPhase.uploading => l10n.messageUploadingVideo, + _VideoUploadPhase.processing => l10n.messageProcessingVideo, + _VideoUploadPhase.ready => l10n.messageReadyToPost, null => null, }; } - String get _postLabel { - if (_uploadErrorMessage != null) return 'Upload failed'; - if (_uploadResult != null) return 'Post'; + String _postLabel(AppLocalizations l10n) { + if (_uploadErrorMessage != null) return l10n.messageUploadFailed; + if (_uploadResult != null) return l10n.buttonPost; final percent = (_uploadProgress * 100).round(); switch (_uploadPhase) { case _VideoUploadPhase.uploading: - return 'Uploading $percent%'; + return l10n.messageUploadingPercent(percent); case _VideoUploadPhase.processing: - return 'Processing video'; + return l10n.messageProcessingVideo; case _VideoUploadPhase.ready: - return 'Post'; + return l10n.buttonPost; case null: - return 'Uploading video'; + return l10n.messageUploadingVideo; } } @@ -326,13 +327,14 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final rawAspectRatio = _player?.value.aspectRatio; final ar = rawAspectRatio != null && rawAspectRatio > 0 ? rawAspectRatio : 1.0; final textLength = _descriptionController.text.runes.length; final isOverLimit = textLength > AppConstants.postDescriptionMaxChars; - final uploadStatusLabel = _uploadStatusLabel; + final uploadStatusLabel = _uploadStatusLabel(l10n); final canPost = !_isPosting && _uploadResult != null && @@ -340,7 +342,7 @@ !isOverLimit; return VideoReviewPageTemplate( - title: 'Review Video', + title: l10n.pageTitleReviewVideo, onBack: () => context.maybePop(), aspectRatio: ar, videoPreview: _player == null @@ -362,7 +364,7 @@ showCrossPost: !widget.storyMode, crossPostValue: _crosspostToBsky, onCrossPostChanged: (v) => setState(() => _crosspostToBsky = v), - postLabel: _postLabel, + postLabel: _postLabel(l10n), isPosting: _isPosting, isOverLimit: isOverLimit, onPost: canPost diff --git a/lib/src/features/posting/ui/widgets/mention_input_field.dart b/lib/src/features/posting/ui/widgets/mention_input_field.dart --- a/lib/src/features/posting/ui/widgets/mention_input_field.dart +++ b/lib/src/features/posting/ui/widgets/mention_input_field.dart @@ -11,7 +11,7 @@ const MentionInputField({ required this.controller, required this.onMentionsChanged, - this.hintText = 'Add a description... (optional)', + required this.hintText, this.maxChars = AppConstants.postDescriptionMaxChars, this.maxLines = 5, this.minLines = 1, diff --git a/lib/src/features/profile/ui/pages/profile_page.dart b/lib/src/features/profile/ui/pages/profile_page.dart --- a/lib/src/features/profile/ui/pages/profile_page.dart +++ b/lib/src/features/profile/ui/pages/profile_page.dart @@ -19,6 +19,7 @@ import 'package:spark/src/core/utils/blocking_utils.dart'; import 'package:spark/src/core/utils/logging/log_service.dart'; import 'package:spark/src/core/utils/logging/logger.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/utils/text_formatter.dart'; import 'package:spark/src/features/auth/providers/auth_providers.dart'; import 'package:spark/src/features/posting/ui/pages/recording_page.dart'; @@ -237,9 +238,10 @@ currentUserDid != null && currentUserDid == widget.did; final colorScheme = theme.colorScheme; + final l10n = AppLocalizations.of(context); return ErrorScreen( context: context, - message: 'Profile not found', + message: l10n.errorProfileNotFound, stackTrace: null, onRetry: notifier.refreshProfile, theme: theme, @@ -378,28 +380,38 @@ // Show confirmation dialog final confirmed = await showDialog( context: context, - builder: (context) => AlertDialog( - title: Text(wasBlocked ? 'Unblock User' : 'Block User'), - content: Text( - wasBlocked - ? 'Are you sure you want to unblock this user?' - : 'Are you sure you want to block this user? ' - 'You will no longer see their posts.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), + builder: (context) { + final dialogL10n = AppLocalizations.of(context); + return AlertDialog( + title: Text( + wasBlocked + ? dialogL10n.dialogUnblockUser + : dialogL10n.dialogBlockUser, ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - style: TextButton.styleFrom( - foregroundColor: wasBlocked ? null : Colors.red, + content: Text( + wasBlocked + ? dialogL10n.dialogUnblockUserConfirm + : dialogL10n.dialogBlockUserConfirm, + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(dialogL10n.buttonCancel), ), - child: Text(wasBlocked ? 'Unblock' : 'Block'), - ), - ], - ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: TextButton.styleFrom( + foregroundColor: wasBlocked ? null : Colors.red, + ), + child: Text( + wasBlocked + ? dialogL10n.buttonUnblock + : dialogL10n.buttonBlock, + ), + ), + ], + ); + }, ); if (confirmed != true) return; @@ -672,7 +684,10 @@ textAlign: TextAlign.center, ), const SizedBox(height: 16), - TextButton(onPressed: onRetry, child: const Text('Retry')), + TextButton( + onPressed: onRetry, + child: Text(AppLocalizations.of(context).buttonRetry), + ), ], ), ), diff --git a/lib/src/features/profile/ui/pages/standalone_likes_feed_page.dart b/lib/src/features/profile/ui/pages/standalone_likes_feed_page.dart --- a/lib/src/features/profile/ui/pages/standalone_likes_feed_page.dart +++ b/lib/src/features/profile/ui/pages/standalone_likes_feed_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/design_system/components/atoms/buttons/app_overlay_back_button.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/design_system/tokens/constants.dart'; import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/core/ui/foundation/colors.dart'; @@ -148,7 +149,9 @@ ), const SizedBox(height: 16), Text( - 'Error loading likes: $error', + AppLocalizations.of( + context, + ).errorWithDetail(error.toString()), style: const TextStyle(color: AppColors.white), textAlign: TextAlign.center, ), @@ -164,7 +167,7 @@ ) .refresh(); }, - child: const Text('Retry'), + child: Text(AppLocalizations.of(context).buttonRetry), ), ], ), diff --git a/lib/src/features/profile/ui/pages/standalone_profile_feed_page.dart b/lib/src/features/profile/ui/pages/standalone_profile_feed_page.dart --- a/lib/src/features/profile/ui/pages/standalone_profile_feed_page.dart +++ b/lib/src/features/profile/ui/pages/standalone_profile_feed_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/design_system/components/atoms/buttons/app_overlay_back_button.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/design_system/tokens/constants.dart'; import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/core/ui/foundation/colors.dart'; @@ -153,7 +154,9 @@ ), const SizedBox(height: 16), Text( - 'Error loading feed: $error', + AppLocalizations.of( + context, + ).errorWithDetail(error.toString()), style: const TextStyle(color: AppColors.white), textAlign: TextAlign.center, ), @@ -170,7 +173,7 @@ ) .refresh(); }, - child: const Text('Retry'), + child: Text(AppLocalizations.of(context).buttonRetry), ), ], ), diff --git a/lib/src/features/profile/ui/pages/standalone_reposts_feed_page.dart b/lib/src/features/profile/ui/pages/standalone_reposts_feed_page.dart --- a/lib/src/features/profile/ui/pages/standalone_reposts_feed_page.dart +++ b/lib/src/features/profile/ui/pages/standalone_reposts_feed_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/design_system/components/atoms/buttons/app_overlay_back_button.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/design_system/tokens/constants.dart'; import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/core/ui/foundation/colors.dart'; @@ -150,7 +151,9 @@ ), const SizedBox(height: 16), Text( - 'Error loading reposts: $error', + AppLocalizations.of( + context, + ).errorWithDetail(error.toString()), style: const TextStyle(color: AppColors.white), textAlign: TextAlign.center, ), @@ -166,7 +169,7 @@ ) .refresh(); }, - child: const Text('Retry'), + child: Text(AppLocalizations.of(context).buttonRetry), ), ], ), diff --git a/lib/src/features/profile/ui/pages/user_list_page.dart b/lib/src/features/profile/ui/pages/user_list_page.dart --- a/lib/src/features/profile/ui/pages/user_list_page.dart +++ b/lib/src/features/profile/ui/pages/user_list_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/design_system/components/atoms/buttons/app_leading_button.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/features/profile/providers/user_list_provider.dart'; import 'package:spark/src/features/profile/ui/widgets/user_list_view.dart'; @@ -49,13 +50,14 @@ final userListAsync = ref.watch( userListProvider(did: widget.did, type: widget.type), ); + final l10n = AppLocalizations.of(context); final title = widget.type == UserListType.followers - ? 'Followers' - : 'Following'; + ? l10n.pageTitleFollowers + : l10n.pageTitleFollowing; return Scaffold( appBar: AppBar( - leading: const AppLeadingButton(tooltip: 'Back'), + leading: AppLeadingButton(tooltip: l10n.tooltipBack), title: Text(title), ), body: RefreshIndicator( @@ -80,7 +82,11 @@ Center( child: Padding( padding: const EdgeInsets.all(16), - child: Text('An error occurred: $error'), + child: Text( + AppLocalizations.of( + context, + ).errorWithDetail(error.toString()), + ), ), ), ], diff --git a/lib/src/features/profile/ui/pages/user_profile_page.dart b/lib/src/features/profile/ui/pages/user_profile_page.dart --- a/lib/src/features/profile/ui/pages/user_profile_page.dart +++ b/lib/src/features/profile/ui/pages/user_profile_page.dart @@ -1,6 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/features/auth/providers/auth_providers.dart'; import 'package:spark/src/features/profile/ui/pages/profile_page.dart'; @@ -13,9 +14,8 @@ final currentUserDid = ref.watch(currentDidProvider); if (currentUserDid == null) { - return const Scaffold( - body: Center(child: Text('Please log in to view your profile')), - ); + final l10n = AppLocalizations.of(context); + return Scaffold(body: Center(child: Text(l10n.messagePleaseLogin))); } // Use the existing ProfilePage but pass the current user's DID diff --git a/lib/src/features/profile/ui/widgets/profile_grid_widget.dart b/lib/src/features/profile/ui/widgets/profile_grid_widget.dart --- a/lib/src/features/profile/ui/widgets/profile_grid_widget.dart +++ b/lib/src/features/profile/ui/widgets/profile_grid_widget.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spark/src/core/design_system/components/molecules/post_tile.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/features/profile/providers/profile_feed_provider.dart'; @@ -123,7 +124,9 @@ children: [ const Icon(FluentIcons.error_circle_24_regular, size: 48), const SizedBox(height: 16), - Text('Error loading posts: $error'), + Text( + AppLocalizations.of(context).errorWithDetail(error.toString()), + ), const SizedBox(height: 16), ElevatedButton( onPressed: () => ref @@ -135,7 +138,7 @@ ).notifier, ) .refresh(), - child: const Text('Retry'), + child: Text(AppLocalizations.of(context).buttonRetry), ), ], ), diff --git a/lib/src/features/profile/ui/widgets/profile_likes_tab.dart b/lib/src/features/profile/ui/widgets/profile_likes_tab.dart --- a/lib/src/features/profile/ui/widgets/profile_likes_tab.dart +++ b/lib/src/features/profile/ui/widgets/profile_likes_tab.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spark/src/core/design_system/components/atoms/icons.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/features/profile/providers/profile_likes_provider.dart'; import 'package:spark/src/features/profile/ui/widgets/profile_grid_widget.dart'; @@ -166,13 +167,17 @@ children: [ const Icon(Icons.error_outline, size: 48), const SizedBox(height: 16), - Text('Error loading likes: $error'), + Text( + AppLocalizations.of( + context, + ).errorWithDetail(error.toString()), + ), const SizedBox(height: 16), ElevatedButton( onPressed: () => ref .read(profileLikesProvider(actor, bsky).notifier) .refresh(), - child: const Text('Retry'), + child: Text(AppLocalizations.of(context).buttonRetry), ), ], ), diff --git a/lib/src/features/profile/ui/widgets/profile_reposts_tab.dart b/lib/src/features/profile/ui/widgets/profile_reposts_tab.dart --- a/lib/src/features/profile/ui/widgets/profile_reposts_tab.dart +++ b/lib/src/features/profile/ui/widgets/profile_reposts_tab.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spark/src/core/design_system/components/atoms/icons.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/features/profile/providers/profile_reposts_provider.dart'; import 'package:spark/src/features/profile/ui/widgets/profile_grid_widget.dart'; @@ -166,13 +167,17 @@ children: [ const Icon(Icons.error_outline, size: 48), const SizedBox(height: 16), - Text('Error loading reposts: $error'), + Text( + AppLocalizations.of( + context, + ).errorWithDetail(error.toString()), + ), const SizedBox(height: 16), ElevatedButton( onPressed: () => ref .read(profileRepostsProvider(actor, bsky).notifier) .refresh(), - child: const Text('Retry'), + child: Text(AppLocalizations.of(context).buttonRetry), ), ], ), diff --git a/lib/src/features/search/ui/pages/search_page.dart b/lib/src/features/search/ui/pages/search_page.dart --- a/lib/src/features/search/ui/pages/search_page.dart +++ b/lib/src/features/search/ui/pages/search_page.dart @@ -129,10 +129,10 @@ : null, ), showTabs: showSubmittedResults, - tabsWidget: const TabBar( + tabsWidget: TabBar( tabs: [ - Tab(text: 'Posts'), - Tab(text: 'Users'), + Tab(text: l10n.tabPosts), + Tab(text: l10n.tabUsers), ], ), contentWidget: const TabBarView( diff --git a/lib/src/features/settings/ui/pages/feed_list_page.dart b/lib/src/features/settings/ui/pages/feed_list_page.dart --- a/lib/src/features/settings/ui/pages/feed_list_page.dart +++ b/lib/src/features/settings/ui/pages/feed_list_page.dart @@ -52,7 +52,7 @@ }, icon: Icon(_isEditMode ? Icons.check : Icons.edit, size: 18), label: Text( - _isEditMode ? 'Done' : 'Edit', + _isEditMode ? l10n.buttonDone : l10n.buttonEdit, style: const TextStyle(fontSize: 14), ), style: TextButton.styleFrom( diff --git a/lib/src/features/settings/ui/pages/labeler_label_settings_page.dart b/lib/src/features/settings/ui/pages/labeler_label_settings_page.dart --- a/lib/src/features/settings/ui/pages/labeler_label_settings_page.dart +++ b/lib/src/features/settings/ui/pages/labeler_label_settings_page.dart @@ -11,6 +11,7 @@ import 'package:spark/src/core/network/atproto/data/models/labeler_models.dart'; import 'package:spark/src/core/network/atproto/data/repositories/actor_repository.dart'; import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/utils/logging/logging.dart'; import 'package:spark/src/features/settings/providers/settings_provider.dart'; import 'package:spark/src/features/settings/ui/widgets/widgets.dart'; @@ -289,6 +290,7 @@ Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; + final l10n = AppLocalizations.of(context); if (_isLoading) { return Scaffold( @@ -302,7 +304,7 @@ titleTextStyle: theme.appBarTheme.titleTextStyle?.copyWith( color: colorScheme.onSurface, ), - title: const Text('Labeler Settings'), + title: Text(l10n.pageTitleLabelerSettings), centerTitle: true, leading: const AppLeadingButton(), ), @@ -322,7 +324,7 @@ titleTextStyle: theme.appBarTheme.titleTextStyle?.copyWith( color: colorScheme.onSurface, ), - title: const Text('Labeler Settings'), + title: Text(l10n.pageTitleLabelerSettings), centerTitle: true, leading: const AppLeadingButton(), ), @@ -335,7 +337,7 @@ Icon(Icons.error_outline, size: 48, color: colorScheme.error), const SizedBox(height: 16), Text( - 'Error Loading Labeler Settings', + l10n.errorLoadingLabelerSettings, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -351,7 +353,7 @@ const SizedBox(height: 16), ElevatedButton( onPressed: _loadLabelerSettings, - child: const Text('Retry'), + child: Text(l10n.buttonRetry), ), ], ), @@ -371,7 +373,7 @@ titleTextStyle: theme.appBarTheme.titleTextStyle?.copyWith( color: colorScheme.onSurface, ), - title: const Text('Labeler Settings'), + title: Text(l10n.pageTitleLabelerSettings), centerTitle: true, leading: const AppLeadingButton(), ), @@ -457,7 +459,7 @@ crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Content Label Settings', + l10n.labelContentLabelSettings, style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, @@ -466,8 +468,7 @@ ), const SizedBox(height: 8), Text( - "Configure how this labeler's content labels " - 'are handled in your feeds.', + l10n.messageLabelerConfigDescription, style: TextStyle( color: colorScheme.onSurface.withAlpha(178), fontSize: 14, diff --git a/lib/src/features/settings/ui/pages/legal_page.dart b/lib/src/features/settings/ui/pages/legal_page.dart --- a/lib/src/features/settings/ui/pages/legal_page.dart +++ b/lib/src/features/settings/ui/pages/legal_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:spark/src/core/design_system/components/atoms/buttons/app_leading_button.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/utils/logging/log_service.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -10,15 +11,25 @@ class LegalPage extends StatelessWidget { const LegalPage({super.key}); - static const List<({String title, String path})> _legalLinks = [ - (title: 'Privacy Policy', path: '/privacy'), - (title: 'Terms of Service', path: '/terms'), - (title: 'Support', path: '/support'), - ]; + static const List _legalPaths = ['/privacy', '/terms', '/support']; static final Uri _baseUri = Uri.parse('https://sprk.so'); + String _titleForPath(String path, AppLocalizations l10n) { + switch (path) { + case '/privacy': + return l10n.labelPrivacyPolicy; + case '/terms': + return l10n.labelTermsOfService; + case '/support': + return l10n.labelSupport; + default: + return path; + } + } + Future _openLink(BuildContext context, String path) async { + final l10n = AppLocalizations.of(context); final logger = GetIt.instance().getLogger('LegalPage'); final uri = _baseUri.replace(path: path); @@ -29,9 +40,9 @@ ); if (!didLaunch && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Unable to open link right now.')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.errorUnableToOpenLink))); } } catch (error, stackTrace) { logger.e( @@ -41,9 +52,9 @@ ); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Unable to open link right now.')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.errorUnableToOpenLink))); } } } @@ -51,6 +62,7 @@ @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context); return Scaffold( backgroundColor: colorScheme.surface, @@ -59,15 +71,15 @@ elevation: 0, scrolledUnderElevation: 0, leading: const AppLeadingButton(), - title: const Text('Legal'), + title: Text(l10n.pageTitleLegal), centerTitle: true, ), body: ListView.separated( padding: const EdgeInsets.all(16), - itemCount: _legalLinks.length, + itemCount: _legalPaths.length, separatorBuilder: (_, _) => const SizedBox(height: 16), itemBuilder: (context, index) { - final link = _legalLinks[index]; + final path = _legalPaths[index]; return Container( decoration: BoxDecoration( @@ -76,19 +88,19 @@ ), child: ListTile( title: Text( - link.title, + _titleForPath(path, l10n), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), - subtitle: Text('sprk.so${link.path}'), + subtitle: Text('sprk.so$path'), trailing: const Icon(FluentIcons.open_24_regular), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 4, ), - onTap: () => _openLink(context, link.path), + onTap: () => _openLink(context, path), ), ); }, diff --git a/lib/src/features/settings/ui/pages/settings_page.dart b/lib/src/features/settings/ui/pages/settings_page.dart --- a/lib/src/features/settings/ui/pages/settings_page.dart +++ b/lib/src/features/settings/ui/pages/settings_page.dart @@ -52,25 +52,22 @@ } Future _handleManageAccount() async { + final l10n = AppLocalizations.of(context); final authRepository = GetIt.instance(); final pdsUrl = authRepository.pdsEndpoint ?? 'your PDS URL'; final shouldOpen = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Open Bluesky account management?'), - content: Text( - 'This opens the Bluesky account management screen. ' - 'You may have to log in again.\n\n' - 'If prompted for an account provider, use:\n$pdsUrl', - ), + title: Text(l10n.dialogOpenBlueskyAccount), + content: Text(l10n.dialogOpenBlueskyAccountDescription(pdsUrl)), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), + child: Text(l10n.buttonCancel), ), FilledButton( onPressed: () => Navigator.of(context).pop(true), - child: const Text('Open'), + child: Text(l10n.buttonOpen), ), ], ), @@ -89,9 +86,9 @@ ); if (!didLaunch && mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Unable to open link right now.')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.errorUnableToOpenLink))); } } catch (error, stackTrace) { logger.e( @@ -101,9 +98,9 @@ ); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Unable to open link right now.')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.errorUnableToOpenLink))); } } } diff --git a/lib/src/features/sound/ui/pages/sound_page.dart b/lib/src/features/sound/ui/pages/sound_page.dart --- a/lib/src/features/sound/ui/pages/sound_page.dart +++ b/lib/src/features/sound/ui/pages/sound_page.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/design_system/components/atoms/buttons/app_leading_button.dart'; import 'package:spark/src/core/design_system/components/molecules/post_tile.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/models.dart'; import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/features/sound/providers/sound_page_provider.dart'; @@ -51,11 +52,12 @@ final soundState = ref.watch(soundPageProvider(_audioAtUri)); final theme = Theme.of(context); final colorScheme = theme.colorScheme; + final l10n = AppLocalizations.of(context); return Scaffold( appBar: AppBar( centerTitle: true, - title: const Text('Sound'), + title: Text(l10n.pageTitleSound), elevation: 0, leading: const AppLeadingButton(), ), @@ -93,7 +95,7 @@ ), const SizedBox(height: 16), Text( - 'No videos using this sound yet', + l10n.emptyNoVideosUsingSound, style: theme.textTheme.bodyLarge?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -153,12 +155,12 @@ children: [ const Icon(FluentIcons.error_circle_24_regular, size: 48), const SizedBox(height: 16), - Text('Error loading sound: $error'), + Text(l10n.errorWithDetail(error.toString())), const SizedBox(height: 16), ElevatedButton( onPressed: () => ref.read(soundPageProvider(_audioAtUri).notifier).refresh(), - child: const Text('Retry'), + child: Text(l10n.buttonRetry), ), ], ), diff --git a/lib/src/features/stories/ui/pages/all_stories_page.dart b/lib/src/features/stories/ui/pages/all_stories_page.dart --- a/lib/src/features/stories/ui/pages/all_stories_page.dart +++ b/lib/src/features/stories/ui/pages/all_stories_page.dart @@ -1,5 +1,6 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/features/stories/ui/pages/author_stories_page.dart'; @@ -40,7 +41,8 @@ @override Widget build(BuildContext context) { if (_authorsList.isEmpty) { - return const Scaffold(body: Center(child: Text('No stories'))); + final l10n = AppLocalizations.of(context); + return Scaffold(body: Center(child: Text(l10n.emptyNoStories))); } return Scaffold( diff --git a/lib/src/features/stories/ui/pages/author_stories_page.dart b/lib/src/features/stories/ui/pages/author_stories_page.dart --- a/lib/src/features/stories/ui/pages/author_stories_page.dart +++ b/lib/src/features/stories/ui/pages/author_stories_page.dart @@ -4,6 +4,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/routing/app_router.dart'; @@ -248,7 +249,8 @@ @override Widget build(BuildContext context) { if (widget.stories.isEmpty) { - return const Scaffold(body: Center(child: Text('No stories'))); + final l10n = AppLocalizations.of(context); + return Scaffold(body: Center(child: Text(l10n.emptyNoStories))); } return Scaffold( diff --git a/lib/src/features/stories/ui/widgets/stories_list.dart b/lib/src/features/stories/ui/widgets/stories_list.dart --- a/lib/src/features/stories/ui/widgets/stories_list.dart +++ b/lib/src/features/stories/ui/widgets/stories_list.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/design_system/components/molecules/story_circle.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/features/auth/providers/auth_providers.dart'; import 'package:spark/src/features/posting/ui/pages/recording_page.dart'; @@ -46,7 +47,7 @@ const Spacer(), IconButton( icon: const Icon(Icons.manage_history_outlined, size: 20), - tooltip: 'Manage', + tooltip: AppLocalizations.of(context).tooltipManage, onPressed: () => context.router.push(const StoryManagerRoute()), ), ], diff --git a/lib/src/core/pro_video_editor/ui/widgets/common/build_stickers.dart b/lib/src/core/pro_video_editor/ui/widgets/common/build_stickers.dart --- a/lib/src/core/pro_video_editor/ui/widgets/common/build_stickers.dart +++ b/lib/src/core/pro_video_editor/ui/widgets/common/build_stickers.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:pro_image_editor/pro_image_editor.dart'; import 'package:spark/src/core/design_system/tokens/colors.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; class DemoBuildStickers extends StatefulWidget { const DemoBuildStickers({ @@ -47,7 +48,7 @@ children: [ const _DragHandle(), _SheetHeader( - title: 'Stickers', + title: AppLocalizations.of(context).labelStickers, onClose: () => Navigator.of(context).pop(), ), Padding( diff --git a/lib/src/core/pro_video_editor/ui/widgets/common/video_progress_alert.dart b/lib/src/core/pro_video_editor/ui/widgets/common/video_progress_alert.dart --- a/lib/src/core/pro_video_editor/ui/widgets/common/video_progress_alert.dart +++ b/lib/src/core/pro_video_editor/ui/widgets/common/video_progress_alert.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:pro_video_editor/pro_video_editor.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; /// A dialog that displays real-time export progress for video generation. /// @@ -68,7 +69,7 @@ mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('Exporting video…'), + Text(AppLocalizations.of(context).messageExportingVideo), const SizedBox(height: 6), Text( '$percent%', diff --git a/lib/src/core/pro_video_editor/ui/widgets/layout/video_toolbar.dart b/lib/src/core/pro_video_editor/ui/widgets/layout/video_toolbar.dart --- a/lib/src/core/pro_video_editor/ui/widgets/layout/video_toolbar.dart +++ b/lib/src/core/pro_video_editor/ui/widgets/layout/video_toolbar.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:spark/src/core/design_system/tokens/colors.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; class VideoToolbar extends StatelessWidget { const VideoToolbar({ @@ -27,6 +28,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); return ColoredBox( color: AppColors.greyBlack, child: SafeArea( @@ -38,43 +40,47 @@ children: [ _ToolButton( icon: Icons.music_note, - label: 'Sound', + label: l10n.labelSound, onPressed: onSound, ), _ToolButton( icon: Icons.brush, - label: 'Paint', + label: l10n.labelPaint, onPressed: onPaint, ), _ToolButton( icon: Icons.text_fields, - label: 'Text', + label: l10n.labelText, onPressed: onText, ), _ToolButton( icon: Icons.crop_rotate, - label: 'Crop', + label: l10n.labelCrop, onPressed: onCrop, ), - _ToolButton(icon: Icons.tune, label: 'Tune', onPressed: onTune), + _ToolButton( + icon: Icons.tune, + label: l10n.labelTune, + onPressed: onTune, + ), _ToolButton( icon: Icons.filter, - label: 'Filter', + label: l10n.labelFilter, onPressed: onFilter, ), _ToolButton( icon: Icons.blur_on, - label: 'Blur', + label: l10n.labelBlur, onPressed: onBlur, ), _ToolButton( icon: Icons.emoji_emotions, - label: 'Emoji', + label: l10n.labelEmoji, onPressed: onEmoji, ), _ToolButton( icon: Icons.star, - label: 'Stickers', + label: l10n.labelStickers, onPressed: onStickers, ), ], diff --git a/lib/src/core/pro_video_editor/ui/widgets/player/preview_video.dart b/lib/src/core/pro_video_editor/ui/widgets/player/preview_video.dart --- a/lib/src/core/pro_video_editor/ui/widgets/player/preview_video.dart +++ b/lib/src/core/pro_video_editor/ui/widgets/player/preview_video.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:pro_image_editor/pro_image_editor.dart'; import 'package:pro_video_editor/pro_video_editor.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:video_player/video_player.dart'; /// A widget that previews a video from a file path. @@ -71,14 +72,15 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); return Scaffold( - appBar: AppBar(title: const Text('Result')), + appBar: AppBar(title: Text(l10n.pageTitleResult)), body: LayoutBuilder( builder: (context, constraints) { return Stack( children: [ Positioned.fill(child: _buildVideoPlayer(constraints)), - _buildGenerationInfos(), + _buildGenerationInfos(l10n), ], ); }, @@ -122,7 +124,7 @@ ); } - Widget _buildGenerationInfos() { + Widget _buildGenerationInfos(AppLocalizations l10n) { const tableSpace = TableRow(children: [SizedBox(height: 3), SizedBox()]); return Positioned( top: 10, @@ -146,7 +148,7 @@ children: [ TableRow( children: [ - const Text('Generation time:'), + Text(l10n.labelGenerationTime), Text('${_generationTime}ms', style: _valueStyle), ], ), @@ -154,21 +156,21 @@ if (md != null) ...[ TableRow( children: [ - const Text('Duration:'), + Text(l10n.labelDuration), Text(md.duration.toString(), style: _valueStyle), ], ), tableSpace, TableRow( children: [ - const Text('Size:'), + Text(l10n.labelSize), Text(_formatBytes(md.fileSize), style: _valueStyle), ], ), tableSpace, TableRow( children: [ - const Text('Resolution:'), + Text(l10n.labelResolution), Text( '${md.resolution.width.toInt()}x' '${md.resolution.height.toInt()}', diff --git a/lib/src/features/feed/ui/widgets/action_buttons/share_panel.dart b/lib/src/features/feed/ui/widgets/action_buttons/share_panel.dart --- a/lib/src/features/feed/ui/widgets/action_buttons/share_panel.dart +++ b/lib/src/features/feed/ui/widgets/action_buttons/share_panel.dart @@ -6,6 +6,7 @@ import 'package:share_plus/share_plus.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spark/src/core/design_system/components/atoms/buttons/long_button.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/utils/logging/log_service.dart'; import 'package:spark/src/features/messages/providers/conversation_provider.dart'; import 'package:spark/src/features/messages/providers/conversations_provider.dart'; @@ -86,6 +87,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final textColor = theme.colorScheme.onSurface; final dividerColor = theme.colorScheme.outline.withValues(alpha: 0.2); @@ -124,7 +126,7 @@ Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Text( - 'Share', + l10n.labelShare, style: TextStyle( color: textColor, fontSize: 18, @@ -142,7 +144,7 @@ return Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 0), child: Text( - 'No conversations yet', + l10n.emptyNoConversations, style: TextStyle(color: textColor.withAlpha(153)), ), ); @@ -172,7 +174,7 @@ error: (e, st) => Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 0), child: Text( - 'Failed to load conversations', + l10n.errorLoadingConversations, style: TextStyle(color: theme.colorScheme.error), ), ), @@ -217,7 +219,9 @@ key: const ValueKey('selected-actions'), width: double.infinity, child: LongButton( - label: _sending ? 'Sending...' : 'Send', + label: _sending + ? l10n.messageSending + : l10n.buttonSend, onPressed: _sending ? null : _sendToSelectedConversation, @@ -228,7 +232,9 @@ children: [ Expanded( child: LongButton( - label: _copiedLink ? 'Copied' : 'Copy link', + label: _copiedLink + ? l10n.buttonCopied + : l10n.buttonCopyLink, onPressed: _copyLink, variant: LongButtonVariant.regular, ), @@ -236,7 +242,7 @@ const SizedBox(width: 12), Expanded( child: LongButton( - label: 'Share', + label: l10n.buttonShare, onPressed: _shareNatively, variant: LongButtonVariant.regular, ), diff --git a/lib/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart b/lib/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart --- a/lib/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart +++ b/lib/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart @@ -7,6 +7,7 @@ import 'package:share_plus/share_plus.dart'; import 'package:spark/src/core/auth/data/repositories/auth_repository.dart'; import 'package:spark/src/core/design_system/components/organisms/side_action_bar.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/network/atproto/atproto.dart'; import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/core/ui/widgets/options_panel.dart'; @@ -313,22 +314,20 @@ Future _handleDeletePost() async { final currentPost = _currentPost ?? widget.post; + final l10n = AppLocalizations.of(context); final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Delete Post'), - content: const Text( - 'Are you sure you want to delete this post?' - '\nThis action cannot be undone.', - ), + title: Text(l10n.dialogDeletePost), + content: Text(l10n.dialogDeletePostConfirm), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), + child: Text(l10n.buttonCancel), ), TextButton( onPressed: () => Navigator.of(context).pop(true), - child: const Text('Delete'), + child: Text(l10n.buttonDelete), ), ], ), @@ -359,29 +358,29 @@ final currentPost = _currentPost ?? widget.post; final author = currentPost.author; final wasBlocked = isBlocking(author.viewer); + final l10n = AppLocalizations.of(context); // Show confirmation dialog final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( - title: Text(wasBlocked ? 'Unblock User' : 'Block User'), + title: Text(wasBlocked ? l10n.dialogUnblockUser : l10n.dialogBlockUser), content: Text( wasBlocked - ? 'Are you sure you want to unblock this user?' - : 'Are you sure you want to block this user? ' - 'You will no longer see their posts.', + ? l10n.dialogUnblockUserConfirm + : l10n.dialogBlockUserConfirm, ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), + child: Text(l10n.buttonCancel), ), TextButton( onPressed: () => Navigator.of(context).pop(true), style: TextButton.styleFrom( foregroundColor: wasBlocked ? null : Colors.red, ), - child: Text(wasBlocked ? 'Unblock' : 'Block'), + child: Text(wasBlocked ? l10n.buttonUnblock : l10n.buttonBlock), ), ], ), diff --git a/lib/src/features/feed/ui/widgets/feed/feeds_bar.dart b/lib/src/features/feed/ui/widgets/feed/feeds_bar.dart --- a/lib/src/features/feed/ui/widgets/feed/feeds_bar.dart +++ b/lib/src/features/feed/ui/widgets/feed/feeds_bar.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/design_system/components/molecules/create_media_sheet.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/design_system/components/molecules/feed_tag_list.dart'; import 'package:spark/src/core/design_system/templates/feeds_bar_template.dart'; import 'package:spark/src/core/media/create_media_actions.dart'; @@ -53,6 +54,7 @@ borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (context) { + final l10n = AppLocalizations.of(context); return SafeArea( child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), @@ -78,7 +80,7 @@ vertical: 8, ), child: Text( - feed.view?.displayName ?? 'Following', + feed.view?.displayName ?? l10n.labelFollowing, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), @@ -92,7 +94,9 @@ isLiked ? Icons.favorite : Icons.favorite_border, color: isLiked ? Colors.red : null, ), - title: Text(isLiked ? 'Unlike Feed' : 'Like Feed'), + title: Text( + isLiked ? l10n.buttonUnlikeFeed : l10n.buttonLikeFeed, + ), onTap: () async { Navigator.pop(context); if (isLiked) { @@ -113,9 +117,9 @@ Icons.delete_outline, color: Colors.red, ), - title: const Text( - 'Remove Feed', - style: TextStyle(color: Colors.red), + title: Text( + l10n.dialogRemoveFeed, + style: const TextStyle(color: Colors.red), ), onTap: () async { Navigator.pop(context); @@ -123,22 +127,23 @@ final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Remove Feed'), + title: Text(l10n.dialogRemoveFeed), content: Text( - 'Are you sure you want to remove ' - '"${feed.view?.displayName ?? 'this feed'}"?', + l10n.dialogRemoveFeedConfirm( + feed.view?.displayName ?? l10n.labelFollowing, + ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), + child: Text(l10n.buttonCancel), ), TextButton( onPressed: () => Navigator.pop(context, true), style: TextButton.styleFrom( foregroundColor: Colors.red, ), - child: const Text('Remove'), + child: Text(l10n.buttonRemove), ), ], ), @@ -153,7 +158,7 @@ // Cancel option ListTile( leading: const Icon(Icons.close), - title: const Text('Cancel'), + title: Text(l10n.buttonCancel), onTap: () => Navigator.pop(context), ), ], @@ -166,6 +171,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final settings = ref.watch(settingsProvider); // Only show pinned feeds in the home view @@ -178,7 +184,7 @@ feed.type == 'timeline' && feed.config.value == 'following'; return FeedTagData( id: feed.config.id, - text: feed.view != null ? feed.view!.displayName : 'Following', + text: feed.view != null ? feed.view!.displayName : l10n.labelFollowing, isTimeline: isTimeline, isLiked: feed.view?.viewer?.like != null, canDelete: !isTimeline, diff --git a/lib/src/features/feed/ui/widgets/post/alt_text_dialog.dart b/lib/src/features/feed/ui/widgets/post/alt_text_dialog.dart --- a/lib/src/features/feed/ui/widgets/post/alt_text_dialog.dart +++ b/lib/src/features/feed/ui/widgets/post/alt_text_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; /// A dialog that displays the alt text (image description) for an image. class AltTextDialog extends StatelessWidget { @@ -8,6 +9,7 @@ @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l10n = AppLocalizations.of(context); return SafeArea( left: false, @@ -40,7 +42,7 @@ ), ), ), - Text('Image Description', style: theme.textTheme.titleLarge), + Text(l10n.hintImageDescription, style: theme.textTheme.titleLarge), const SizedBox(height: 12), Text( altText, diff --git a/lib/src/features/feed/ui/widgets/post/feed_post_widget.dart b/lib/src/features/feed/ui/widgets/post/feed_post_widget.dart --- a/lib/src/features/feed/ui/widgets/post/feed_post_widget.dart +++ b/lib/src/features/feed/ui/widgets/post/feed_post_widget.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; +import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/core/ui/foundation/colors.dart'; import 'package:spark/src/core/ui/widgets/content_warning_overlay.dart'; @@ -152,6 +153,7 @@ @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); // Check if we need to reload post due to state changes final feedState = ref.watch(feedProvider(widget.feed)); final navigationState = ref.watch(navigationProvider); @@ -345,7 +347,7 @@ decoration: const BoxDecoration(color: AppColors.black), child: Center( child: Text( - 'Error loading post: ${snapshot.error}', + l10n.errorWithDetail(snapshot.error.toString()), style: const TextStyle(color: Colors.white), ), ),