[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: ensure localization use

+2068 -286
+17 -13
lib/src/core/design_system/components/molecules/feed_card.dart
··· 6 6 import 'package:spark/src/core/design_system/tokens/colors.dart'; 7 7 import 'package:spark/src/core/design_system/tokens/shapes.dart'; 8 8 import 'package:spark/src/core/design_system/tokens/typography.dart'; 9 + import 'package:spark/src/core/l10n/app_localizations.dart'; 9 10 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 10 11 11 12 class FeedCard extends StatelessWidget { ··· 34 35 35 36 bool get _isTimeline => feed.type == 'timeline'; 36 37 37 - String get _title { 38 + String _getTitle(AppLocalizations l10n) { 38 39 if (generator != null) { 39 40 return generator!.displayName; 40 41 } 41 - return _isTimeline ? 'Following' : feed.config.value; 42 + return _isTimeline ? l10n.labelFollowing : feed.config.value; 42 43 } 43 44 44 - String? get _subtitle { 45 + String? _getSubtitle(AppLocalizations l10n) { 45 46 if (generator != null) { 46 - return 'by @${generator!.creator.handle}'; 47 + return l10n.labelFeedByCreator(generator!.creator.handle); 47 48 } 48 49 if (_isTimeline) { 49 - return 'Posts from people you follow'; 50 + return l10n.messagePostsFromFollowing; 50 51 } 51 52 return null; 52 53 } ··· 62 63 63 64 bool get _isPrimaryAction => !isAdded || !isPinned; 64 65 65 - String get _actionLabel { 66 - if (!isAdded) return 'Add feed'; 67 - if (isPinned) return 'Unpin feed'; 68 - return 'Pin feed'; 66 + String _getActionLabel(AppLocalizations l10n) { 67 + if (!isAdded) return l10n.buttonAddFeed; 68 + if (isPinned) return l10n.buttonUnpinFeed; 69 + return l10n.buttonPinFeed; 69 70 } 70 71 71 72 VoidCallback get _actionCallback { ··· 127 128 } 128 129 129 130 Widget _buildTextContent(BuildContext context) { 131 + final l10n = AppLocalizations.of(context); 132 + final subtitle = _getSubtitle(l10n); 130 133 return Column( 131 134 mainAxisSize: MainAxisSize.min, 132 135 crossAxisAlignment: CrossAxisAlignment.start, 133 136 children: [ 134 137 Text( 135 - _title, 138 + _getTitle(l10n), 136 139 style: AppTypography.textSmallBold, 137 140 overflow: TextOverflow.ellipsis, 138 141 ), 139 - if (_subtitle != null) 142 + if (subtitle != null) 140 143 Text( 141 - _subtitle!, 144 + subtitle, 142 145 style: AppTypography.textSmallThin, 143 146 overflow: TextOverflow.ellipsis, 144 147 ), ··· 180 183 } 181 184 182 185 Widget _buildActionButton(BuildContext context) { 186 + final l10n = AppLocalizations.of(context); 183 187 final isDark = Theme.of(context).brightness == Brightness.dark; 184 188 final isPrimary = _isPrimaryAction; 185 189 final backgroundColor = isPrimary ··· 218 222 ), 219 223 alignment: Alignment.center, 220 224 child: Text( 221 - _actionLabel, 225 + _getActionLabel(l10n), 222 226 style: textStyle, 223 227 overflow: TextOverflow.ellipsis, 224 228 ),
+3 -1
lib/src/core/design_system/components/molecules/profile_action_buttons.dart
··· 3 3 import 'package:spark/src/core/design_system/components/atoms/toggles/follow_button.dart'; 4 4 import 'package:spark/src/core/design_system/tokens/colors.dart'; 5 5 import 'package:spark/src/core/design_system/tokens/typography.dart'; 6 + import 'package:spark/src/core/l10n/app_localizations.dart'; 6 7 7 8 class ProfileActionButtons extends StatelessWidget { 8 9 const ProfileActionButtons({ ··· 26 27 27 28 @override 28 29 Widget build(BuildContext context) { 30 + final l10n = AppLocalizations.of(context); 29 31 if (isCurrentUser) { 30 32 return Row( 31 33 children: [Expanded(child: _EditButton(onTap: onEditTap))], ··· 41 43 onFollow: onFollowTap ?? () {}, 42 44 onUnfollow: onUnfollowTap ?? () {}, 43 45 onUnblock: onUnblockTap, 44 - unfollowText: 'Following', 46 + unfollowText: l10n.labelFollowing, 45 47 width: double.infinity, 46 48 ), 47 49 ),
+8 -3
lib/src/core/design_system/components/molecules/profile_stats.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:skeletonizer/skeletonizer.dart'; 3 3 import 'package:spark/src/core/design_system/tokens/typography.dart'; 4 + import 'package:spark/src/core/l10n/app_localizations.dart'; 4 5 5 6 class ProfileStats extends StatelessWidget { 6 7 const ProfileStats({ ··· 20 21 21 22 @override 22 23 Widget build(BuildContext context) { 24 + final l10n = AppLocalizations.of(context); 23 25 return Row( 24 26 mainAxisAlignment: MainAxisAlignment.spaceEvenly, 25 27 children: [ 26 - _StatItem(count: postsCount, label: 'Posts'), 28 + _StatItem(count: postsCount, label: l10n.labelPosts), 27 29 GestureDetector( 28 30 onTap: onFollowersTap, 29 31 behavior: HitTestBehavior.opaque, 30 - child: _StatItem(count: followersCount, label: 'Followers'), 32 + child: _StatItem( 33 + count: followersCount, 34 + label: l10n.pageTitleFollowers, 35 + ), 31 36 ), 32 37 GestureDetector( 33 38 onTap: onFollowingTap, 34 39 behavior: HitTestBehavior.opaque, 35 - child: _StatItem(count: followingCount, label: 'Following'), 40 + child: _StatItem(count: followingCount, label: l10n.labelFollowing), 36 41 ), 37 42 ], 38 43 );
+8 -6
lib/src/core/design_system/components/molecules/settings_feed_card.dart
··· 5 5 import 'package:spark/src/core/design_system/tokens/colors.dart'; 6 6 import 'package:spark/src/core/design_system/tokens/shapes.dart'; 7 7 import 'package:spark/src/core/design_system/tokens/typography.dart'; 8 + import 'package:spark/src/core/l10n/app_localizations.dart'; 8 9 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 9 10 10 11 enum SettingsFeedCardMode { display, edit } ··· 41 42 42 43 bool get _isLiked => feed.view?.viewer?.like != null; 43 44 44 - String get _title { 45 + String _getTitle(AppLocalizations l10n) { 45 46 if (generator != null) { 46 47 return generator!.displayName; 47 48 } 48 - return _isTimeline ? 'Following' : feed.config.value; 49 + return _isTimeline ? l10n.labelFollowing : feed.config.value; 49 50 } 50 51 51 52 String? get _subtitle { ··· 101 102 children: [ 102 103 _buildAvatar(), 103 104 const SizedBox(width: 12), 104 - Expanded(child: _buildTextContent()), 105 + Expanded(child: _buildTextContent(context)), 105 106 const SizedBox(width: 8), 106 107 _buildLikeButton(), 107 108 ], ··· 134 135 children: [ 135 136 _buildAvatar(), 136 137 const SizedBox(width: 12), 137 - Expanded(child: _buildTextContent()), 138 + Expanded(child: _buildTextContent(context)), 138 139 const SizedBox(width: 8), 139 140 _buildEditActions(), 140 141 const SizedBox(width: 8), ··· 185 186 ); 186 187 } 187 188 188 - Widget _buildTextContent() { 189 + Widget _buildTextContent(BuildContext context) { 190 + final l10n = AppLocalizations.of(context); 189 191 return Column( 190 192 crossAxisAlignment: CrossAxisAlignment.start, 191 193 mainAxisSize: MainAxisSize.min, 192 194 children: [ 193 195 Text( 194 - _title, 196 + _getTitle(l10n), 195 197 style: AppTypography.textSmallBold, 196 198 overflow: TextOverflow.ellipsis, 197 199 maxLines: 1,
+3 -1
lib/src/core/design_system/templates/chat_thread_page_template.dart
··· 3 3 import 'package:spark/src/core/design_system/components/molecules/input_field.dart'; 4 4 import 'package:spark/src/core/design_system/components/molecules/profile_avatar.dart'; 5 5 import 'package:spark/src/core/design_system/tokens/typography.dart'; 6 + import 'package:spark/src/core/l10n/app_localizations.dart'; 6 7 7 8 class ChatThreadPageTemplate extends StatelessWidget { 8 9 const ChatThreadPageTemplate({ ··· 25 26 @override 26 27 Widget build(BuildContext context) { 27 28 final theme = Theme.of(context); 29 + final l10n = AppLocalizations.of(context); 28 30 29 31 return Scaffold( 30 32 backgroundColor: theme.colorScheme.surface, ··· 77 79 padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), 78 80 child: InputField.chat( 79 81 controller: textController, 80 - hintText: 'Message...', 82 + hintText: l10n.hintMessage, 81 83 onSendMessage: onSend, 82 84 ), 83 85 ),
+6 -2
lib/src/core/design_system/templates/image_review_page_template.dart
··· 8 8 import 'package:spark/src/core/design_system/tokens/colors.dart'; 9 9 import 'package:spark/src/core/design_system/tokens/shapes.dart'; 10 10 import 'package:spark/src/core/design_system/tokens/typography.dart'; 11 + import 'package:spark/src/core/l10n/app_localizations.dart'; 11 12 import 'package:spark/src/features/posting/models/mention_controller.dart'; 12 13 import 'package:spark/src/features/posting/ui/widgets/mention_input_field.dart'; 13 14 ··· 73 74 74 75 @override 75 76 Widget build(BuildContext context) { 77 + final l10n = AppLocalizations.of(context); 76 78 final theme = Theme.of(context); 77 79 final colorScheme = theme.colorScheme; 78 80 ··· 83 85 elevation: 0, 84 86 leading: AppLeadingButton( 85 87 color: theme.textTheme.titleLarge?.color, 86 - tooltip: 'Back', 88 + tooltip: l10n.buttonBack, 87 89 ), 88 90 title: Text(title), 89 91 centerTitle: false, ··· 368 370 369 371 @override 370 372 Widget build(BuildContext context) { 373 + final l10n = AppLocalizations.of(context); 371 374 final textController = mentionController?.textController ?? controller; 372 375 final count = textController?.text.runes.length ?? 0; 373 376 final showCounter = count >= (maxChars * 0.8); ··· 381 384 MentionInputField( 382 385 controller: mentionController!, 383 386 onMentionsChanged: onMentionsChanged ?? (_) {}, 387 + hintText: l10n.hintAddDescription, 384 388 ) 385 389 else if (controller != null) 386 390 InputField.search( 387 391 controller: controller!, 388 - hintText: 'Add a description... (optional)', 392 + hintText: l10n.hintAddDescription, 389 393 maxLines: 5, 390 394 minLines: 1, 391 395 ),
+7 -3
lib/src/core/design_system/templates/video_review_page_template.dart
··· 5 5 import 'package:spark/src/core/design_system/tokens/colors.dart'; 6 6 import 'package:spark/src/core/design_system/tokens/shapes.dart'; 7 7 import 'package:spark/src/core/design_system/tokens/typography.dart'; 8 + import 'package:spark/src/core/l10n/app_localizations.dart'; 8 9 import 'package:spark/src/features/posting/models/mention_controller.dart'; 9 10 import 'package:spark/src/features/posting/ui/widgets/mention_input_field.dart'; 10 11 ··· 65 66 66 67 @override 67 68 Widget build(BuildContext context) { 69 + final l10n = AppLocalizations.of(context); 68 70 final theme = Theme.of(context); 69 71 final colorScheme = theme.colorScheme; 70 72 ··· 75 77 elevation: 0, 76 78 leading: AppLeadingButton( 77 79 color: theme.textTheme.titleLarge?.color, 78 - tooltip: 'Back', 80 + tooltip: l10n.buttonBack, 79 81 ), 80 82 title: Text(title), 81 83 centerTitle: false, ··· 226 228 alignment: Alignment.centerRight, 227 229 child: TextButton( 228 230 onPressed: onRetry, 229 - child: const Text('Try again'), 231 + child: Text(AppLocalizations.of(context).buttonTryAgain), 230 232 ), 231 233 ), 232 234 ], ··· 317 319 318 320 @override 319 321 Widget build(BuildContext context) { 322 + final l10n = AppLocalizations.of(context); 320 323 final textController = mentionController?.textController ?? controller; 321 324 final count = textController?.text.runes.length ?? 0; 322 325 final showCounter = count >= (maxChars * 0.8); ··· 330 333 MentionInputField( 331 334 controller: mentionController!, 332 335 onMentionsChanged: onMentionsChanged ?? (_) {}, 336 + hintText: l10n.hintAddDescription, 333 337 ) 334 338 else if (controller != null) 335 339 InputField.search( 336 340 controller: controller!, 337 - hintText: 'Add a description... (optional)', 341 + hintText: l10n.hintAddDescription, 338 342 maxLines: 5, 339 343 minLines: 1, 340 344 ),
+655 -1
lib/src/core/l10n/app_localizations.dart
··· 439 439 /// Add description placeholder 440 440 /// 441 441 /// In en, this message translates to: 442 - /// **'Add a description... (optional)'** 442 + /// **'Add a caption... (optional)'** 443 443 String get hintAddDescription; 444 444 445 445 /// Add alt text placeholder ··· 651 651 /// In en, this message translates to: 652 652 /// **'Other'** 653 653 String get categoryOther; 654 + 655 + /// Continue button text 656 + /// 657 + /// In en, this message translates to: 658 + /// **'Continue'** 659 + String get buttonContinue; 660 + 661 + /// Get started button text 662 + /// 663 + /// In en, this message translates to: 664 + /// **'Get Started'** 665 + String get buttonGetStarted; 666 + 667 + /// Already have an account button text 668 + /// 669 + /// In en, this message translates to: 670 + /// **'I already have an account'** 671 + String get buttonHaveAccount; 672 + 673 + /// Open button text 674 + /// 675 + /// In en, this message translates to: 676 + /// **'Open'** 677 + String get buttonOpen; 678 + 679 + /// Post button text 680 + /// 681 + /// In en, this message translates to: 682 + /// **'Post'** 683 + String get buttonPost; 684 + 685 + /// Done button text 686 + /// 687 + /// In en, this message translates to: 688 + /// **'Done'** 689 + String get buttonDone; 690 + 691 + /// Edit button text 692 + /// 693 + /// In en, this message translates to: 694 + /// **'Edit'** 695 + String get buttonEdit; 696 + 697 + /// Back button text 698 + /// 699 + /// In en, this message translates to: 700 + /// **'Back'** 701 + String get buttonBack; 702 + 703 + /// Share button text 704 + /// 705 + /// In en, this message translates to: 706 + /// **'Share'** 707 + String get buttonShare; 708 + 709 + /// Copied state text for copy link 710 + /// 711 + /// In en, this message translates to: 712 + /// **'Copied'** 713 + String get buttonCopied; 714 + 715 + /// Copy link button text 716 + /// 717 + /// In en, this message translates to: 718 + /// **'Copy link'** 719 + String get buttonCopyLink; 720 + 721 + /// Error message for invalid handle 722 + /// 723 + /// In en, this message translates to: 724 + /// **'Invalid handle'** 725 + String get errorInvalidHandle; 726 + 727 + /// Error message for handle not found 728 + /// 729 + /// In en, this message translates to: 730 + /// **'Could not find this handle'** 731 + String get errorHandleNotFound; 732 + 733 + /// Loading message when completing sign in 734 + /// 735 + /// In en, this message translates to: 736 + /// **'Completing sign in...'** 737 + String get errorCompletingSignIn; 738 + 739 + /// Error message for profile not found 740 + /// 741 + /// In en, this message translates to: 742 + /// **'Profile not found'** 743 + String get errorProfileNotFound; 744 + 745 + /// Error loading post message 746 + /// 747 + /// In en, this message translates to: 748 + /// **'Error loading post'** 749 + String get errorLoadingPost; 750 + 751 + /// Error loading messages message 752 + /// 753 + /// In en, this message translates to: 754 + /// **'Failed to load messages'** 755 + String get errorLoadingMessages; 756 + 757 + /// Error loading conversations message 758 + /// 759 + /// In en, this message translates to: 760 + /// **'Failed to load conversations'** 761 + String get errorLoadingConversations; 762 + 763 + /// Error message when unable to open a link 764 + /// 765 + /// In en, this message translates to: 766 + /// **'Unable to open link right now.'** 767 + String get errorUnableToOpenLink; 768 + 769 + /// Error loading labeler settings message 770 + /// 771 + /// In en, this message translates to: 772 + /// **'Error Loading Labeler Settings'** 773 + String get errorLoadingLabelerSettings; 774 + 775 + /// Error message with detail 776 + /// 777 + /// In en, this message translates to: 778 + /// **'Error: {error}'** 779 + String errorWithDetail(String error); 780 + 781 + /// Add a comment input placeholder 782 + /// 783 + /// In en, this message translates to: 784 + /// **'Add a comment...'** 785 + String get hintAddComment; 786 + 787 + /// Search users placeholder in messages 788 + /// 789 + /// In en, this message translates to: 790 + /// **'Search users'** 791 + String get hintSearchUsersMessages; 792 + 793 + /// Delete comment confirmation message 794 + /// 795 + /// In en, this message translates to: 796 + /// **'Are you sure you want to delete this comment? This action cannot be undone.'** 797 + String get dialogDeleteCommentConfirm; 798 + 799 + /// Delete post confirmation message 800 + /// 801 + /// In en, this message translates to: 802 + /// **'Are you sure you want to delete this post? This action cannot be undone.'** 803 + String get dialogDeletePostConfirm; 804 + 805 + /// Block user dialog title 806 + /// 807 + /// In en, this message translates to: 808 + /// **'Block User'** 809 + String get dialogBlockUser; 810 + 811 + /// Block user confirmation message 812 + /// 813 + /// In en, this message translates to: 814 + /// **'Are you sure you want to block this user? You will no longer see their posts.'** 815 + String get dialogBlockUserConfirm; 816 + 817 + /// Unblock user dialog title 818 + /// 819 + /// In en, this message translates to: 820 + /// **'Unblock User'** 821 + String get dialogUnblockUser; 822 + 823 + /// Unblock user confirmation message 824 + /// 825 + /// In en, this message translates to: 826 + /// **'Are you sure you want to unblock this user?'** 827 + String get dialogUnblockUserConfirm; 828 + 829 + /// Remove feed confirmation message with name 830 + /// 831 + /// In en, this message translates to: 832 + /// **'Are you sure you want to remove \"{name}\"?'** 833 + String dialogRemoveFeedConfirm(String name); 834 + 835 + /// Dialog title for opening Bluesky account management 836 + /// 837 + /// In en, this message translates to: 838 + /// **'Open Bluesky account management?'** 839 + String get dialogOpenBlueskyAccount; 840 + 841 + /// Description for opening Bluesky account management dialog 842 + /// 843 + /// In en, this message translates to: 844 + /// **'This opens the Bluesky account management screen. You may have to log in again.\n\nIf prompted for an account provider, use:\n{pdsUrl}'** 845 + String dialogOpenBlueskyAccountDescription(String pdsUrl); 846 + 847 + /// Replies page title 848 + /// 849 + /// In en, this message translates to: 850 + /// **'Replies'** 851 + String get pageTitleReplies; 852 + 853 + /// Review video page title 854 + /// 855 + /// In en, this message translates to: 856 + /// **'Review Video'** 857 + String get pageTitleReviewVideo; 858 + 859 + /// Review image post page title 860 + /// 861 + /// In en, this message translates to: 862 + /// **'Review Image Post'** 863 + String get pageTitleReviewImagePost; 864 + 865 + /// Legal page title 866 + /// 867 + /// In en, this message translates to: 868 + /// **'Legal'** 869 + String get pageTitleLegal; 870 + 871 + /// Followers page title 872 + /// 873 + /// In en, this message translates to: 874 + /// **'Followers'** 875 + String get pageTitleFollowers; 876 + 877 + /// Following page title 878 + /// 879 + /// In en, this message translates to: 880 + /// **'Following'** 881 + String get pageTitleFollowing; 882 + 883 + /// Empty state for no videos using a sound 884 + /// 885 + /// In en, this message translates to: 886 + /// **'No videos using this sound yet'** 887 + String get emptyNoVideosUsingSound; 888 + 889 + /// Empty state for no photos in library 890 + /// 891 + /// In en, this message translates to: 892 + /// **'No photos or videos found in your library.'** 893 + String get emptyNoPhotoLibrary; 894 + 895 + /// Permission message for photo library access 896 + /// 897 + /// In en, this message translates to: 898 + /// **'Allow photo library access to pick photos and videos.'** 899 + String get messagePermissionPhotoLibrary; 900 + 901 + /// Posting story progress message 902 + /// 903 + /// In en, this message translates to: 904 + /// **'Posting story...'** 905 + String get messagePostingStory; 906 + 907 + /// Processing video progress message 908 + /// 909 + /// In en, this message translates to: 910 + /// **'Processing video...'** 911 + String get messageProcessingVideo; 912 + 913 + /// Uploading video status message 914 + /// 915 + /// In en, this message translates to: 916 + /// **'Uploading video'** 917 + String get messageUploadingVideo; 918 + 919 + /// Ready to post status message 920 + /// 921 + /// In en, this message translates to: 922 + /// **'Ready to post'** 923 + String get messageReadyToPost; 924 + 925 + /// Upload failed status message 926 + /// 927 + /// In en, this message translates to: 928 + /// **'Upload failed'** 929 + String get messageUploadFailed; 930 + 931 + /// Uploading percentage status message 932 + /// 933 + /// In en, this message translates to: 934 + /// **'Uploading {percent}%'** 935 + String messageUploadingPercent(int percent); 936 + 937 + /// Original sound label 938 + /// 939 + /// In en, this message translates to: 940 + /// **'Original Sound'** 941 + String get labelOriginalSound; 942 + 943 + /// Share label 944 + /// 945 + /// In en, this message translates to: 946 + /// **'Share'** 947 + String get labelShare; 948 + 949 + /// Following label 950 + /// 951 + /// In en, this message translates to: 952 + /// **'Following'** 953 + String get labelFollowing; 954 + 955 + /// Posts label 956 + /// 957 + /// In en, this message translates to: 958 + /// **'Posts'** 959 + String get labelPosts; 960 + 961 + /// Privacy policy link label 962 + /// 963 + /// In en, this message translates to: 964 + /// **'Privacy Policy'** 965 + String get labelPrivacyPolicy; 966 + 967 + /// Terms of service link label 968 + /// 969 + /// In en, this message translates to: 970 + /// **'Terms of Service'** 971 + String get labelTermsOfService; 972 + 973 + /// Support link label 974 + /// 975 + /// In en, this message translates to: 976 + /// **'Support'** 977 + String get labelSupport; 978 + 979 + /// Back tooltip 980 + /// 981 + /// In en, this message translates to: 982 + /// **'Back'** 983 + String get tooltipBack; 984 + 985 + /// Error message for failed image load 986 + /// 987 + /// In en, this message translates to: 988 + /// **'Failed to load image'** 989 + String get errorFailedToLoadImage; 990 + 991 + /// Sign in page title 992 + /// 993 + /// In en, this message translates to: 994 + /// **'Sign In'** 995 + String get pageTitleSignIn; 996 + 997 + /// Message to enter handle for OAuth 998 + /// 999 + /// In en, this message translates to: 1000 + /// **'Enter your handle to continue with OAuth'** 1001 + String get messageEnterHandle; 1002 + 1003 + /// Loading message when completing sign up 1004 + /// 1005 + /// In en, this message translates to: 1006 + /// **'Completing sign up...'** 1007 + String get messageCompletingSignUp; 1008 + 1009 + /// Welcome message on register page 1010 + /// 1011 + /// In en, this message translates to: 1012 + /// **'Welcome!'** 1013 + String get messageWelcome; 1014 + 1015 + /// Welcome description on register page 1016 + /// 1017 + /// In en, this message translates to: 1018 + /// **'Share videos, connect with friends,\nand take back your timeline.'** 1019 + String get messageWelcomeDescription; 1020 + 1021 + /// Reply button label 1022 + /// 1023 + /// In en, this message translates to: 1024 + /// **'Reply'** 1025 + String get labelReply; 1026 + 1027 + /// Add image tooltip 1028 + /// 1029 + /// In en, this message translates to: 1030 + /// **'Add image (1 max)'** 1031 + String get hintAddImage; 1032 + 1033 + /// Posting image progress message 1034 + /// 1035 + /// In en, this message translates to: 1036 + /// **'Posting...'** 1037 + String get messagePostingImage; 1038 + 1039 + /// Maximum images reached tooltip 1040 + /// 1041 + /// In en, this message translates to: 1042 + /// **'Maximum images reached'** 1043 + String get messageMaximumImagesReached; 1044 + 1045 + /// Sound label for video editor toolbar 1046 + /// 1047 + /// In en, this message translates to: 1048 + /// **'Sound'** 1049 + String get labelSound; 1050 + 1051 + /// Stickers label 1052 + /// 1053 + /// In en, this message translates to: 1054 + /// **'Stickers'** 1055 + String get labelStickers; 1056 + 1057 + /// Paint editor label 1058 + /// 1059 + /// In en, this message translates to: 1060 + /// **'Paint'** 1061 + String get labelPaint; 1062 + 1063 + /// Text editor label 1064 + /// 1065 + /// In en, this message translates to: 1066 + /// **'Text'** 1067 + String get labelText; 1068 + 1069 + /// Crop editor label 1070 + /// 1071 + /// In en, this message translates to: 1072 + /// **'Crop'** 1073 + String get labelCrop; 1074 + 1075 + /// Tune editor label 1076 + /// 1077 + /// In en, this message translates to: 1078 + /// **'Tune'** 1079 + String get labelTune; 1080 + 1081 + /// Filter editor label 1082 + /// 1083 + /// In en, this message translates to: 1084 + /// **'Filter'** 1085 + String get labelFilter; 1086 + 1087 + /// Blur editor label 1088 + /// 1089 + /// In en, this message translates to: 1090 + /// **'Blur'** 1091 + String get labelBlur; 1092 + 1093 + /// Emoji editor label 1094 + /// 1095 + /// In en, this message translates to: 1096 + /// **'Emoji'** 1097 + String get labelEmoji; 1098 + 1099 + /// Mention editor label 1100 + /// 1101 + /// In en, this message translates to: 1102 + /// **'Mention'** 1103 + String get labelMention; 1104 + 1105 + /// Draw editor label 1106 + /// 1107 + /// In en, this message translates to: 1108 + /// **'Draw'** 1109 + String get labelDraw; 1110 + 1111 + /// Default video clip title 1112 + /// 1113 + /// In en, this message translates to: 1114 + /// **'My awesome video'** 1115 + String get labelMyAwesomeVideo; 1116 + 1117 + /// Video upload status when uploading 1118 + /// 1119 + /// In en, this message translates to: 1120 + /// **'Uploading video'** 1121 + String get errorUploadingVideo; 1122 + 1123 + /// Video processing status 1124 + /// 1125 + /// In en, this message translates to: 1126 + /// **'Processing video'** 1127 + String get errorProcessingVideoStatus; 1128 + 1129 + /// Ready to post status 1130 + /// 1131 + /// In en, this message translates to: 1132 + /// **'Ready to post'** 1133 + String get errorReadyToPost; 1134 + 1135 + /// Upload failed status 1136 + /// 1137 + /// In en, this message translates to: 1138 + /// **'Upload failed'** 1139 + String get errorUploadFailed; 1140 + 1141 + /// Error message in snackbar 1142 + /// 1143 + /// In en, this message translates to: 1144 + /// **'Error: {error}'** 1145 + String errorSnackBar(String error); 1146 + 1147 + /// Like feed button text 1148 + /// 1149 + /// In en, this message translates to: 1150 + /// **'Like Feed'** 1151 + String get buttonLikeFeed; 1152 + 1153 + /// Unlike feed button text 1154 + /// 1155 + /// In en, this message translates to: 1156 + /// **'Unlike Feed'** 1157 + String get buttonUnlikeFeed; 1158 + 1159 + /// Empty state for no notifications 1160 + /// 1161 + /// In en, this message translates to: 1162 + /// **'No notifications'** 1163 + String get emptyNoNotifications; 1164 + 1165 + /// Message when all notifications are read 1166 + /// 1167 + /// In en, this message translates to: 1168 + /// **'You\'\'re all caught up!'** 1169 + String get messageAllCaughtUp; 1170 + 1171 + /// Description for labeler configuration 1172 + /// 1173 + /// In en, this message translates to: 1174 + /// **'Configure how this labeler\'\'s content labels are handled in your feeds.'** 1175 + String get messageLabelerConfigDescription; 1176 + 1177 + /// Error loading notifications message 1178 + /// 1179 + /// In en, this message translates to: 1180 + /// **'Failed to load notifications'** 1181 + String get errorLoadingNotifications; 1182 + 1183 + /// Content label settings title 1184 + /// 1185 + /// In en, this message translates to: 1186 + /// **'Content Label Settings'** 1187 + String get labelContentLabelSettings; 1188 + 1189 + /// Error when trying to select photos in single-select mode 1190 + /// 1191 + /// In en, this message translates to: 1192 + /// **'You can only select photos in multi-select mode.'** 1193 + String get errorPhotoSelectLimit; 1194 + 1195 + /// Error when exceeding max photo selection 1196 + /// 1197 + /// In en, this message translates to: 1198 + /// **'You can select up to {max}.'** 1199 + String errorPhotoSelectMax(int max); 1200 + 1201 + /// Error when unable to access selected photos 1202 + /// 1203 + /// In en, this message translates to: 1204 + /// **'Unable to access the selected photos.'** 1205 + String get errorUnableToAccessPhotos; 1206 + 1207 + /// Error when unable to access a media item 1208 + /// 1209 + /// In en, this message translates to: 1210 + /// **'Unable to access this media item.'** 1211 + String get errorUnableToAccessMedia; 1212 + 1213 + /// Single select mode label 1214 + /// 1215 + /// In en, this message translates to: 1216 + /// **'Single Select'** 1217 + String get labelSingleSelect; 1218 + 1219 + /// Select multiple mode label 1220 + /// 1221 + /// In en, this message translates to: 1222 + /// **'Select multiple'** 1223 + String get labelSelectMultiple; 1224 + 1225 + /// Library header label 1226 + /// 1227 + /// In en, this message translates to: 1228 + /// **'Library'** 1229 + String get labelLibrary; 1230 + 1231 + /// Done button with selection count 1232 + /// 1233 + /// In en, this message translates to: 1234 + /// **'Done ({current}/{max})'** 1235 + String labelDoneCount(int current, int max); 1236 + 1237 + /// Permission info about limited library access 1238 + /// 1239 + /// In en, this message translates to: 1240 + /// **'Limited library access is enabled. You can change this in settings.'** 1241 + String get messageLimitedLibraryAccess; 1242 + 1243 + /// Posts search tab label 1244 + /// 1245 + /// In en, this message translates to: 1246 + /// **'Posts'** 1247 + String get tabPosts; 1248 + 1249 + /// Users search tab label 1250 + /// 1251 + /// In en, this message translates to: 1252 + /// **'Users'** 1253 + String get tabUsers; 1254 + 1255 + /// Search by handle or display name placeholder 1256 + /// 1257 + /// In en, this message translates to: 1258 + /// **'Search by handle or display name'** 1259 + String get hintSearchByHandle; 1260 + 1261 + /// Feed creator attribution label 1262 + /// 1263 + /// In en, this message translates to: 1264 + /// **'by @{handle}'** 1265 + String labelFeedByCreator(String handle); 1266 + 1267 + /// Subtitle for following/timeline feed 1268 + /// 1269 + /// In en, this message translates to: 1270 + /// **'Posts from people you follow'** 1271 + String get messagePostsFromFollowing; 1272 + 1273 + /// Add feed button text 1274 + /// 1275 + /// In en, this message translates to: 1276 + /// **'Add feed'** 1277 + String get buttonAddFeed; 1278 + 1279 + /// Unpin feed button text 1280 + /// 1281 + /// In en, this message translates to: 1282 + /// **'Unpin feed'** 1283 + String get buttonUnpinFeed; 1284 + 1285 + /// Pin feed button text 1286 + /// 1287 + /// In en, this message translates to: 1288 + /// **'Pin feed'** 1289 + String get buttonPinFeed; 1290 + 1291 + /// Empty state for no conversations in share panel 1292 + /// 1293 + /// In en, this message translates to: 1294 + /// **'No conversations yet'** 1295 + String get emptyNoConversations; 1296 + 1297 + /// Sending message progress indicator 1298 + /// 1299 + /// In en, this message translates to: 1300 + /// **'Sending...'** 1301 + String get messageSending; 1302 + 1303 + /// Send button text 1304 + /// 1305 + /// In en, this message translates to: 1306 + /// **'Send'** 1307 + String get buttonSend; 654 1308 } 655 1309 656 1310 class _AppLocalizationsDelegate
+355 -1
lib/src/core/l10n/app_localizations_en.dart
··· 183 183 String get hintTypeMessage => 'Type a message...'; 184 184 185 185 @override 186 - String get hintAddDescription => 'Add a description... (optional)'; 186 + String get hintAddDescription => 'Add a caption... (optional)'; 187 187 188 188 @override 189 189 String get hintAddAltText => 'Add alt text'; ··· 298 298 299 299 @override 300 300 String get categoryOther => 'Other'; 301 + 302 + @override 303 + String get buttonContinue => 'Continue'; 304 + 305 + @override 306 + String get buttonGetStarted => 'Get Started'; 307 + 308 + @override 309 + String get buttonHaveAccount => 'I already have an account'; 310 + 311 + @override 312 + String get buttonOpen => 'Open'; 313 + 314 + @override 315 + String get buttonPost => 'Post'; 316 + 317 + @override 318 + String get buttonDone => 'Done'; 319 + 320 + @override 321 + String get buttonEdit => 'Edit'; 322 + 323 + @override 324 + String get buttonBack => 'Back'; 325 + 326 + @override 327 + String get buttonShare => 'Share'; 328 + 329 + @override 330 + String get buttonCopied => 'Copied'; 331 + 332 + @override 333 + String get buttonCopyLink => 'Copy link'; 334 + 335 + @override 336 + String get errorInvalidHandle => 'Invalid handle'; 337 + 338 + @override 339 + String get errorHandleNotFound => 'Could not find this handle'; 340 + 341 + @override 342 + String get errorCompletingSignIn => 'Completing sign in...'; 343 + 344 + @override 345 + String get errorProfileNotFound => 'Profile not found'; 346 + 347 + @override 348 + String get errorLoadingPost => 'Error loading post'; 349 + 350 + @override 351 + String get errorLoadingMessages => 'Failed to load messages'; 352 + 353 + @override 354 + String get errorLoadingConversations => 'Failed to load conversations'; 355 + 356 + @override 357 + String get errorUnableToOpenLink => 'Unable to open link right now.'; 358 + 359 + @override 360 + String get errorLoadingLabelerSettings => 'Error Loading Labeler Settings'; 361 + 362 + @override 363 + String errorWithDetail(String error) { 364 + return 'Error: $error'; 365 + } 366 + 367 + @override 368 + String get hintAddComment => 'Add a comment...'; 369 + 370 + @override 371 + String get hintSearchUsersMessages => 'Search users'; 372 + 373 + @override 374 + String get dialogDeleteCommentConfirm => 375 + 'Are you sure you want to delete this comment? This action cannot be undone.'; 376 + 377 + @override 378 + String get dialogDeletePostConfirm => 379 + 'Are you sure you want to delete this post? This action cannot be undone.'; 380 + 381 + @override 382 + String get dialogBlockUser => 'Block User'; 383 + 384 + @override 385 + String get dialogBlockUserConfirm => 386 + 'Are you sure you want to block this user? You will no longer see their posts.'; 387 + 388 + @override 389 + String get dialogUnblockUser => 'Unblock User'; 390 + 391 + @override 392 + String get dialogUnblockUserConfirm => 393 + 'Are you sure you want to unblock this user?'; 394 + 395 + @override 396 + String dialogRemoveFeedConfirm(String name) { 397 + return 'Are you sure you want to remove \"$name\"?'; 398 + } 399 + 400 + @override 401 + String get dialogOpenBlueskyAccount => 'Open Bluesky account management?'; 402 + 403 + @override 404 + String dialogOpenBlueskyAccountDescription(String pdsUrl) { 405 + 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'; 406 + } 407 + 408 + @override 409 + String get pageTitleReplies => 'Replies'; 410 + 411 + @override 412 + String get pageTitleReviewVideo => 'Review Video'; 413 + 414 + @override 415 + String get pageTitleReviewImagePost => 'Review Image Post'; 416 + 417 + @override 418 + String get pageTitleLegal => 'Legal'; 419 + 420 + @override 421 + String get pageTitleFollowers => 'Followers'; 422 + 423 + @override 424 + String get pageTitleFollowing => 'Following'; 425 + 426 + @override 427 + String get emptyNoVideosUsingSound => 'No videos using this sound yet'; 428 + 429 + @override 430 + String get emptyNoPhotoLibrary => 431 + 'No photos or videos found in your library.'; 432 + 433 + @override 434 + String get messagePermissionPhotoLibrary => 435 + 'Allow photo library access to pick photos and videos.'; 436 + 437 + @override 438 + String get messagePostingStory => 'Posting story...'; 439 + 440 + @override 441 + String get messageProcessingVideo => 'Processing video...'; 442 + 443 + @override 444 + String get messageUploadingVideo => 'Uploading video'; 445 + 446 + @override 447 + String get messageReadyToPost => 'Ready to post'; 448 + 449 + @override 450 + String get messageUploadFailed => 'Upload failed'; 451 + 452 + @override 453 + String messageUploadingPercent(int percent) { 454 + return 'Uploading $percent%'; 455 + } 456 + 457 + @override 458 + String get labelOriginalSound => 'Original Sound'; 459 + 460 + @override 461 + String get labelShare => 'Share'; 462 + 463 + @override 464 + String get labelFollowing => 'Following'; 465 + 466 + @override 467 + String get labelPosts => 'Posts'; 468 + 469 + @override 470 + String get labelPrivacyPolicy => 'Privacy Policy'; 471 + 472 + @override 473 + String get labelTermsOfService => 'Terms of Service'; 474 + 475 + @override 476 + String get labelSupport => 'Support'; 477 + 478 + @override 479 + String get tooltipBack => 'Back'; 480 + 481 + @override 482 + String get errorFailedToLoadImage => 'Failed to load image'; 483 + 484 + @override 485 + String get pageTitleSignIn => 'Sign In'; 486 + 487 + @override 488 + String get messageEnterHandle => 'Enter your handle to continue with OAuth'; 489 + 490 + @override 491 + String get messageCompletingSignUp => 'Completing sign up...'; 492 + 493 + @override 494 + String get messageWelcome => 'Welcome!'; 495 + 496 + @override 497 + String get messageWelcomeDescription => 498 + 'Share videos, connect with friends,\nand take back your timeline.'; 499 + 500 + @override 501 + String get labelReply => 'Reply'; 502 + 503 + @override 504 + String get hintAddImage => 'Add image (1 max)'; 505 + 506 + @override 507 + String get messagePostingImage => 'Posting...'; 508 + 509 + @override 510 + String get messageMaximumImagesReached => 'Maximum images reached'; 511 + 512 + @override 513 + String get labelSound => 'Sound'; 514 + 515 + @override 516 + String get labelStickers => 'Stickers'; 517 + 518 + @override 519 + String get labelPaint => 'Paint'; 520 + 521 + @override 522 + String get labelText => 'Text'; 523 + 524 + @override 525 + String get labelCrop => 'Crop'; 526 + 527 + @override 528 + String get labelTune => 'Tune'; 529 + 530 + @override 531 + String get labelFilter => 'Filter'; 532 + 533 + @override 534 + String get labelBlur => 'Blur'; 535 + 536 + @override 537 + String get labelEmoji => 'Emoji'; 538 + 539 + @override 540 + String get labelMention => 'Mention'; 541 + 542 + @override 543 + String get labelDraw => 'Draw'; 544 + 545 + @override 546 + String get labelMyAwesomeVideo => 'My awesome video'; 547 + 548 + @override 549 + String get errorUploadingVideo => 'Uploading video'; 550 + 551 + @override 552 + String get errorProcessingVideoStatus => 'Processing video'; 553 + 554 + @override 555 + String get errorReadyToPost => 'Ready to post'; 556 + 557 + @override 558 + String get errorUploadFailed => 'Upload failed'; 559 + 560 + @override 561 + String errorSnackBar(String error) { 562 + return 'Error: $error'; 563 + } 564 + 565 + @override 566 + String get buttonLikeFeed => 'Like Feed'; 567 + 568 + @override 569 + String get buttonUnlikeFeed => 'Unlike Feed'; 570 + 571 + @override 572 + String get emptyNoNotifications => 'No notifications'; 573 + 574 + @override 575 + String get messageAllCaughtUp => 'You\'re all caught up!'; 576 + 577 + @override 578 + String get messageLabelerConfigDescription => 579 + 'Configure how this labeler\'s content labels are handled in your feeds.'; 580 + 581 + @override 582 + String get errorLoadingNotifications => 'Failed to load notifications'; 583 + 584 + @override 585 + String get labelContentLabelSettings => 'Content Label Settings'; 586 + 587 + @override 588 + String get errorPhotoSelectLimit => 589 + 'You can only select photos in multi-select mode.'; 590 + 591 + @override 592 + String errorPhotoSelectMax(int max) { 593 + return 'You can select up to $max.'; 594 + } 595 + 596 + @override 597 + String get errorUnableToAccessPhotos => 598 + 'Unable to access the selected photos.'; 599 + 600 + @override 601 + String get errorUnableToAccessMedia => 'Unable to access this media item.'; 602 + 603 + @override 604 + String get labelSingleSelect => 'Single Select'; 605 + 606 + @override 607 + String get labelSelectMultiple => 'Select multiple'; 608 + 609 + @override 610 + String get labelLibrary => 'Library'; 611 + 612 + @override 613 + String labelDoneCount(int current, int max) { 614 + return 'Done ($current/$max)'; 615 + } 616 + 617 + @override 618 + String get messageLimitedLibraryAccess => 619 + 'Limited library access is enabled. You can change this in settings.'; 620 + 621 + @override 622 + String get tabPosts => 'Posts'; 623 + 624 + @override 625 + String get tabUsers => 'Users'; 626 + 627 + @override 628 + String get hintSearchByHandle => 'Search by handle or display name'; 629 + 630 + @override 631 + String labelFeedByCreator(String handle) { 632 + return 'by @$handle'; 633 + } 634 + 635 + @override 636 + String get messagePostsFromFollowing => 'Posts from people you follow'; 637 + 638 + @override 639 + String get buttonAddFeed => 'Add feed'; 640 + 641 + @override 642 + String get buttonUnpinFeed => 'Unpin feed'; 643 + 644 + @override 645 + String get buttonPinFeed => 'Pin feed'; 646 + 647 + @override 648 + String get emptyNoConversations => 'No conversations yet'; 649 + 650 + @override 651 + String get messageSending => 'Sending...'; 652 + 653 + @override 654 + String get buttonSend => 'Send'; 301 655 }
+589 -1
lib/src/core/l10n/intl_en.arb
··· 287 287 "description": "Type message input placeholder" 288 288 }, 289 289 290 - "hintAddDescription": "Add a description... (optional)", 290 + "hintAddDescription": "Add a caption... (optional)", 291 291 "@hintAddDescription": { 292 292 "description": "Add description placeholder" 293 293 }, ··· 485 485 "categoryOther": "Other", 486 486 "@categoryOther": { 487 487 "description": "Report category: Other" 488 + }, 489 + 490 + "buttonContinue": "Continue", 491 + "@buttonContinue": { 492 + "description": "Continue button text" 493 + }, 494 + 495 + "buttonGetStarted": "Get Started", 496 + "@buttonGetStarted": { 497 + "description": "Get started button text" 498 + }, 499 + 500 + "buttonHaveAccount": "I already have an account", 501 + "@buttonHaveAccount": { 502 + "description": "Already have an account button text" 503 + }, 504 + 505 + "buttonOpen": "Open", 506 + "@buttonOpen": { 507 + "description": "Open button text" 508 + }, 509 + 510 + "buttonPost": "Post", 511 + "@buttonPost": { 512 + "description": "Post button text" 513 + }, 514 + 515 + "buttonDone": "Done", 516 + "@buttonDone": { 517 + "description": "Done button text" 518 + }, 519 + 520 + "buttonEdit": "Edit", 521 + "@buttonEdit": { 522 + "description": "Edit button text" 523 + }, 524 + 525 + "buttonBack": "Back", 526 + "@buttonBack": { 527 + "description": "Back button text" 528 + }, 529 + 530 + "buttonShare": "Share", 531 + "@buttonShare": { 532 + "description": "Share button text" 533 + }, 534 + 535 + "buttonCopied": "Copied", 536 + "@buttonCopied": { 537 + "description": "Copied state text for copy link" 538 + }, 539 + 540 + "buttonCopyLink": "Copy link", 541 + "@buttonCopyLink": { 542 + "description": "Copy link button text" 543 + }, 544 + 545 + "errorInvalidHandle": "Invalid handle", 546 + "@errorInvalidHandle": { 547 + "description": "Error message for invalid handle" 548 + }, 549 + 550 + "errorHandleNotFound": "Could not find this handle", 551 + "@errorHandleNotFound": { 552 + "description": "Error message for handle not found" 553 + }, 554 + 555 + "errorCompletingSignIn": "Completing sign in...", 556 + "@errorCompletingSignIn": { 557 + "description": "Loading message when completing sign in" 558 + }, 559 + 560 + "errorProfileNotFound": "Profile not found", 561 + "@errorProfileNotFound": { 562 + "description": "Error message for profile not found" 563 + }, 564 + 565 + "errorLoadingPost": "Error loading post", 566 + "@errorLoadingPost": { 567 + "description": "Error loading post message" 568 + }, 569 + 570 + "errorLoadingMessages": "Failed to load messages", 571 + "@errorLoadingMessages": { 572 + "description": "Error loading messages message" 573 + }, 574 + 575 + "errorLoadingConversations": "Failed to load conversations", 576 + "@errorLoadingConversations": { 577 + "description": "Error loading conversations message" 578 + }, 579 + 580 + "errorUnableToOpenLink": "Unable to open link right now.", 581 + "@errorUnableToOpenLink": { 582 + "description": "Error message when unable to open a link" 583 + }, 584 + 585 + "errorLoadingLabelerSettings": "Error Loading Labeler Settings", 586 + "@errorLoadingLabelerSettings": { 587 + "description": "Error loading labeler settings message" 588 + }, 589 + 590 + "errorWithDetail": "Error: {error}", 591 + "@errorWithDetail": { 592 + "description": "Error message with detail", 593 + "placeholders": { 594 + "error": { 595 + "type": "String" 596 + } 597 + } 598 + }, 599 + 600 + "hintAddComment": "Add a comment...", 601 + "@hintAddComment": { 602 + "description": "Add a comment input placeholder" 603 + }, 604 + 605 + "hintSearchUsersMessages": "Search users", 606 + "@hintSearchUsersMessages": { 607 + "description": "Search users placeholder in messages" 608 + }, 609 + 610 + "dialogDeleteCommentConfirm": "Are you sure you want to delete this comment? This action cannot be undone.", 611 + "@dialogDeleteCommentConfirm": { 612 + "description": "Delete comment confirmation message" 613 + }, 614 + 615 + "dialogDeletePostConfirm": "Are you sure you want to delete this post? This action cannot be undone.", 616 + "@dialogDeletePostConfirm": { 617 + "description": "Delete post confirmation message" 618 + }, 619 + 620 + "dialogBlockUser": "Block User", 621 + "@dialogBlockUser": { 622 + "description": "Block user dialog title" 623 + }, 624 + 625 + "dialogBlockUserConfirm": "Are you sure you want to block this user? You will no longer see their posts.", 626 + "@dialogBlockUserConfirm": { 627 + "description": "Block user confirmation message" 628 + }, 629 + 630 + "dialogUnblockUser": "Unblock User", 631 + "@dialogUnblockUser": { 632 + "description": "Unblock user dialog title" 633 + }, 634 + 635 + "dialogUnblockUserConfirm": "Are you sure you want to unblock this user?", 636 + "@dialogUnblockUserConfirm": { 637 + "description": "Unblock user confirmation message" 638 + }, 639 + 640 + "dialogRemoveFeedConfirm": "Are you sure you want to remove \"{name}\"?", 641 + "@dialogRemoveFeedConfirm": { 642 + "description": "Remove feed confirmation message with name", 643 + "placeholders": { 644 + "name": { 645 + "type": "String" 646 + } 647 + } 648 + }, 649 + 650 + "dialogOpenBlueskyAccount": "Open Bluesky account management?", 651 + "@dialogOpenBlueskyAccount": { 652 + "description": "Dialog title for opening Bluesky account management" 653 + }, 654 + 655 + "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}", 656 + "@dialogOpenBlueskyAccountDescription": { 657 + "description": "Description for opening Bluesky account management dialog", 658 + "placeholders": { 659 + "pdsUrl": { 660 + "type": "String" 661 + } 662 + } 663 + }, 664 + 665 + "pageTitleReplies": "Replies", 666 + "@pageTitleReplies": { 667 + "description": "Replies page title" 668 + }, 669 + 670 + "pageTitleReviewVideo": "Review Video", 671 + "@pageTitleReviewVideo": { 672 + "description": "Review video page title" 673 + }, 674 + 675 + "pageTitleReviewImagePost": "Review Image Post", 676 + "@pageTitleReviewImagePost": { 677 + "description": "Review image post page title" 678 + }, 679 + 680 + "pageTitleLegal": "Legal", 681 + "@pageTitleLegal": { 682 + "description": "Legal page title" 683 + }, 684 + 685 + "pageTitleFollowers": "Followers", 686 + "@pageTitleFollowers": { 687 + "description": "Followers page title" 688 + }, 689 + 690 + "pageTitleFollowing": "Following", 691 + "@pageTitleFollowing": { 692 + "description": "Following page title" 693 + }, 694 + 695 + "emptyNoVideosUsingSound": "No videos using this sound yet", 696 + "@emptyNoVideosUsingSound": { 697 + "description": "Empty state for no videos using a sound" 698 + }, 699 + 700 + "emptyNoPhotoLibrary": "No photos or videos found in your library.", 701 + "@emptyNoPhotoLibrary": { 702 + "description": "Empty state for no photos in library" 703 + }, 704 + 705 + "messagePermissionPhotoLibrary": "Allow photo library access to pick photos and videos.", 706 + "@messagePermissionPhotoLibrary": { 707 + "description": "Permission message for photo library access" 708 + }, 709 + 710 + "messagePostingStory": "Posting story...", 711 + "@messagePostingStory": { 712 + "description": "Posting story progress message" 713 + }, 714 + 715 + "messageProcessingVideo": "Processing video...", 716 + "@messageProcessingVideo": { 717 + "description": "Processing video progress message" 718 + }, 719 + 720 + "messageUploadingVideo": "Uploading video", 721 + "@messageUploadingVideo": { 722 + "description": "Uploading video status message" 723 + }, 724 + 725 + "messageReadyToPost": "Ready to post", 726 + "@messageReadyToPost": { 727 + "description": "Ready to post status message" 728 + }, 729 + 730 + "messageUploadFailed": "Upload failed", 731 + "@messageUploadFailed": { 732 + "description": "Upload failed status message" 733 + }, 734 + 735 + "messageUploadingPercent": "Uploading {percent}%", 736 + "@messageUploadingPercent": { 737 + "description": "Uploading percentage status message", 738 + "placeholders": { 739 + "percent": { 740 + "type": "int" 741 + } 742 + } 743 + }, 744 + 745 + "labelOriginalSound": "Original Sound", 746 + "@labelOriginalSound": { 747 + "description": "Original sound label" 748 + }, 749 + 750 + "labelShare": "Share", 751 + "@labelShare": { 752 + "description": "Share label" 753 + }, 754 + 755 + "labelFollowing": "Following", 756 + "@labelFollowing": { 757 + "description": "Following label" 758 + }, 759 + 760 + "labelPosts": "Posts", 761 + "@labelPosts": { 762 + "description": "Posts label" 763 + }, 764 + 765 + "labelPrivacyPolicy": "Privacy Policy", 766 + "@labelPrivacyPolicy": { 767 + "description": "Privacy policy link label" 768 + }, 769 + 770 + "labelTermsOfService": "Terms of Service", 771 + "@labelTermsOfService": { 772 + "description": "Terms of service link label" 773 + }, 774 + 775 + "labelSupport": "Support", 776 + "@labelSupport": { 777 + "description": "Support link label" 778 + }, 779 + 780 + "tooltipBack": "Back", 781 + "@tooltipBack": { 782 + "description": "Back tooltip" 783 + }, 784 + 785 + "errorFailedToLoadImage": "Failed to load image", 786 + "@errorFailedToLoadImage": { 787 + "description": "Error message for failed image load" 788 + }, 789 + 790 + "pageTitleSignIn": "Sign In", 791 + "@pageTitleSignIn": { 792 + "description": "Sign in page title" 793 + }, 794 + 795 + "messageEnterHandle": "Enter your handle to continue with OAuth", 796 + "@messageEnterHandle": { 797 + "description": "Message to enter handle for OAuth" 798 + }, 799 + 800 + "messageCompletingSignUp": "Completing sign up...", 801 + "@messageCompletingSignUp": { 802 + "description": "Loading message when completing sign up" 803 + }, 804 + 805 + "messageWelcome": "Welcome!", 806 + "@messageWelcome": { 807 + "description": "Welcome message on register page" 808 + }, 809 + 810 + "messageWelcomeDescription": "Share videos, connect with friends,\nand take back your timeline.", 811 + "@messageWelcomeDescription": { 812 + "description": "Welcome description on register page" 813 + }, 814 + 815 + "labelReply": "Reply", 816 + "@labelReply": { 817 + "description": "Reply button label" 818 + }, 819 + 820 + "hintAddImage": "Add image (1 max)", 821 + "@hintAddImage": { 822 + "description": "Add image tooltip" 823 + }, 824 + 825 + "messagePostingImage": "Posting...", 826 + "@messagePostingImage": { 827 + "description": "Posting image progress message" 828 + }, 829 + 830 + "messageMaximumImagesReached": "Maximum images reached", 831 + "@messageMaximumImagesReached": { 832 + "description": "Maximum images reached tooltip" 833 + }, 834 + 835 + "labelSound": "Sound", 836 + "@labelSound": { 837 + "description": "Sound label for video editor toolbar" 838 + }, 839 + 840 + "labelStickers": "Stickers", 841 + "@labelStickers": { 842 + "description": "Stickers label" 843 + }, 844 + 845 + "labelPaint": "Paint", 846 + "@labelPaint": { 847 + "description": "Paint editor label" 848 + }, 849 + 850 + "labelText": "Text", 851 + "@labelText": { 852 + "description": "Text editor label" 853 + }, 854 + 855 + "labelCrop": "Crop", 856 + "@labelCrop": { 857 + "description": "Crop editor label" 858 + }, 859 + 860 + "labelTune": "Tune", 861 + "@labelTune": { 862 + "description": "Tune editor label" 863 + }, 864 + 865 + "labelFilter": "Filter", 866 + "@labelFilter": { 867 + "description": "Filter editor label" 868 + }, 869 + 870 + "labelBlur": "Blur", 871 + "@labelBlur": { 872 + "description": "Blur editor label" 873 + }, 874 + 875 + "labelEmoji": "Emoji", 876 + "@labelEmoji": { 877 + "description": "Emoji editor label" 878 + }, 879 + 880 + "labelMention": "Mention", 881 + "@labelMention": { 882 + "description": "Mention editor label" 883 + }, 884 + 885 + "labelDraw": "Draw", 886 + "@labelDraw": { 887 + "description": "Draw editor label" 888 + }, 889 + 890 + "labelMyAwesomeVideo": "My awesome video", 891 + "@labelMyAwesomeVideo": { 892 + "description": "Default video clip title" 893 + }, 894 + 895 + "errorUploadingVideo": "Uploading video", 896 + "@errorUploadingVideo": { 897 + "description": "Video upload status when uploading" 898 + }, 899 + 900 + "errorProcessingVideoStatus": "Processing video", 901 + "@errorProcessingVideoStatus": { 902 + "description": "Video processing status" 903 + }, 904 + 905 + "errorReadyToPost": "Ready to post", 906 + "@errorReadyToPost": { 907 + "description": "Ready to post status" 908 + }, 909 + 910 + "errorUploadFailed": "Upload failed", 911 + "@errorUploadFailed": { 912 + "description": "Upload failed status" 913 + }, 914 + 915 + "errorSnackBar": "Error: {error}", 916 + "@errorSnackBar": { 917 + "description": "Error message in snackbar", 918 + "placeholders": { 919 + "error": { 920 + "type": "String" 921 + } 922 + } 923 + }, 924 + 925 + "buttonLikeFeed": "Like Feed", 926 + "@buttonLikeFeed": { 927 + "description": "Like feed button text" 928 + }, 929 + 930 + "buttonUnlikeFeed": "Unlike Feed", 931 + "@buttonUnlikeFeed": { 932 + "description": "Unlike feed button text" 933 + }, 934 + 935 + "emptyNoNotifications": "No notifications", 936 + "@emptyNoNotifications": { 937 + "description": "Empty state for no notifications" 938 + }, 939 + 940 + "messageAllCaughtUp": "You''re all caught up!", 941 + "@messageAllCaughtUp": { 942 + "description": "Message when all notifications are read" 943 + }, 944 + 945 + "messageLabelerConfigDescription": "Configure how this labeler''s content labels are handled in your feeds.", 946 + "@messageLabelerConfigDescription": { 947 + "description": "Description for labeler configuration" 948 + }, 949 + 950 + "errorLoadingNotifications": "Failed to load notifications", 951 + "@errorLoadingNotifications": { 952 + "description": "Error loading notifications message" 953 + }, 954 + 955 + "labelContentLabelSettings": "Content Label Settings", 956 + "@labelContentLabelSettings": { 957 + "description": "Content label settings title" 958 + }, 959 + 960 + "errorPhotoSelectLimit": "You can only select photos in multi-select mode.", 961 + "@errorPhotoSelectLimit": { 962 + "description": "Error when trying to select photos in single-select mode" 963 + }, 964 + 965 + "errorPhotoSelectMax": "You can select up to {max}.", 966 + "@errorPhotoSelectMax": { 967 + "description": "Error when exceeding max photo selection", 968 + "placeholders": { 969 + "max": { 970 + "type": "int" 971 + } 972 + } 973 + }, 974 + 975 + "errorUnableToAccessPhotos": "Unable to access the selected photos.", 976 + "@errorUnableToAccessPhotos": { 977 + "description": "Error when unable to access selected photos" 978 + }, 979 + 980 + "errorUnableToAccessMedia": "Unable to access this media item.", 981 + "@errorUnableToAccessMedia": { 982 + "description": "Error when unable to access a media item" 983 + }, 984 + 985 + "labelSingleSelect": "Single Select", 986 + "@labelSingleSelect": { 987 + "description": "Single select mode label" 988 + }, 989 + 990 + "labelSelectMultiple": "Select multiple", 991 + "@labelSelectMultiple": { 992 + "description": "Select multiple mode label" 993 + }, 994 + 995 + "labelLibrary": "Library", 996 + "@labelLibrary": { 997 + "description": "Library header label" 998 + }, 999 + 1000 + "labelDoneCount": "Done ({current}/{max})", 1001 + "@labelDoneCount": { 1002 + "description": "Done button with selection count", 1003 + "placeholders": { 1004 + "current": { 1005 + "type": "int" 1006 + }, 1007 + "max": { 1008 + "type": "int" 1009 + } 1010 + } 1011 + }, 1012 + 1013 + "messageLimitedLibraryAccess": "Limited library access is enabled. You can change this in settings.", 1014 + "@messageLimitedLibraryAccess": { 1015 + "description": "Permission info about limited library access" 1016 + }, 1017 + 1018 + "tabPosts": "Posts", 1019 + "@tabPosts": { 1020 + "description": "Posts search tab label" 1021 + }, 1022 + 1023 + "tabUsers": "Users", 1024 + "@tabUsers": { 1025 + "description": "Users search tab label" 1026 + }, 1027 + 1028 + "hintSearchByHandle": "Search by handle or display name", 1029 + "@hintSearchByHandle": { 1030 + "description": "Search by handle or display name placeholder" 1031 + }, 1032 + 1033 + "labelFeedByCreator": "by @{handle}", 1034 + "@labelFeedByCreator": { 1035 + "description": "Feed creator attribution label", 1036 + "placeholders": { 1037 + "handle": { 1038 + "type": "String" 1039 + } 1040 + } 1041 + }, 1042 + 1043 + "messagePostsFromFollowing": "Posts from people you follow", 1044 + "@messagePostsFromFollowing": { 1045 + "description": "Subtitle for following/timeline feed" 1046 + }, 1047 + 1048 + "buttonAddFeed": "Add feed", 1049 + "@buttonAddFeed": { 1050 + "description": "Add feed button text" 1051 + }, 1052 + 1053 + "buttonUnpinFeed": "Unpin feed", 1054 + "@buttonUnpinFeed": { 1055 + "description": "Unpin feed button text" 1056 + }, 1057 + 1058 + "buttonPinFeed": "Pin feed", 1059 + "@buttonPinFeed": { 1060 + "description": "Pin feed button text" 1061 + }, 1062 + 1063 + "emptyNoConversations": "No conversations yet", 1064 + "@emptyNoConversations": { 1065 + "description": "Empty state for no conversations in share panel" 1066 + }, 1067 + 1068 + "messageSending": "Sending...", 1069 + "@messageSending": { 1070 + "description": "Sending message progress indicator" 1071 + }, 1072 + 1073 + "buttonSend": "Send", 1074 + "@buttonSend": { 1075 + "description": "Send button text" 488 1076 } 489 1077 }
+5 -4
lib/src/core/pro_image_editor/ui/story_image_editor_page.dart
··· 7 7 import 'package:image_picker/image_picker.dart'; 8 8 import 'package:path_provider/path_provider.dart'; 9 9 import 'package:pro_image_editor/pro_image_editor.dart'; 10 + import 'package:spark/src/core/l10n/app_localizations.dart'; 10 11 import 'package:spark/src/core/pro_image_editor/models/story_image_editor_result.dart'; 11 12 import 'package:spark/src/core/pro_image_editor/story_mention_editing.dart'; 12 13 import 'package:spark/src/core/pro_image_editor/story_image_editor_configs.dart'; ··· 171 172 children: [ 172 173 const Icon(Icons.error_outline, color: Colors.red, size: 48), 173 174 const SizedBox(height: 16), 174 - const Text( 175 - 'Failed to load image', 176 - style: TextStyle(color: Colors.white, fontSize: 18), 175 + Text( 176 + AppLocalizations.of(context).errorFailedToLoadImage, 177 + style: const TextStyle(color: Colors.white, fontSize: 18), 177 178 ), 178 179 const SizedBox(height: 8), 179 180 TextButton( 180 181 onPressed: () => Navigator.of(context).pop(), 181 - child: const Text('Go Back'), 182 + child: Text(AppLocalizations.of(context).buttonGoBack), 182 183 ), 183 184 ], 184 185 ),
+17 -7
lib/src/core/pro_image_editor/ui/widgets/story_editor_toolbar.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:spark/src/core/design_system/tokens/colors.dart'; 3 + import 'package:spark/src/core/l10n/app_localizations.dart'; 3 4 4 5 /// Toolbar widget for the Story Image Editor. 5 6 /// ··· 26 27 27 28 @override 28 29 Widget build(BuildContext context) { 30 + final l10n = AppLocalizations.of(context); 29 31 final items = <Widget>[ 30 32 if (onMention != null) 31 33 _ToolbarItem( 32 34 icon: Icons.alternate_email_rounded, 33 - label: 'Mention', 35 + label: l10n.labelMention, 34 36 onTap: () => onMention!.call(), 35 37 ), 36 - _ToolbarItem(icon: Icons.brush_rounded, label: 'Draw', onTap: onPaint), 38 + _ToolbarItem( 39 + icon: Icons.brush_rounded, 40 + label: l10n.labelDraw, 41 + onTap: onPaint, 42 + ), 37 43 _ToolbarItem( 38 44 icon: Icons.text_fields_rounded, 39 - label: 'Text', 45 + label: l10n.labelText, 40 46 onTap: onText, 41 47 ), 42 48 _ToolbarItem( 43 49 icon: Icons.auto_awesome_rounded, 44 - label: 'Filter', 50 + label: l10n.labelFilter, 45 51 onTap: onFilter, 46 52 ), 47 - _ToolbarItem(icon: Icons.blur_on_rounded, label: 'Blur', onTap: onBlur), 53 + _ToolbarItem( 54 + icon: Icons.blur_on_rounded, 55 + label: l10n.labelBlur, 56 + onTap: onBlur, 57 + ), 48 58 _ToolbarItem( 49 59 icon: Icons.emoji_emotions_rounded, 50 - label: 'Emoji', 60 + label: l10n.labelEmoji, 51 61 onTap: onEmoji, 52 62 ), 53 63 _ToolbarItem( 54 64 icon: Icons.sticky_note_2_rounded, 55 - label: 'Stickers', 65 + label: l10n.labelStickers, 56 66 onTap: onStickers, 57 67 ), 58 68 ];
+3 -1
lib/src/core/pro_image_editor/ui/widgets/story_mention_picker_sheet.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 + import 'package:spark/src/core/l10n/app_localizations.dart'; 3 4 import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; 4 5 import 'package:spark/src/features/search/providers/actor_typeahead_provider.dart'; 5 6 import 'package:spark/src/features/search/providers/actor_typeahead_state.dart'; ··· 38 39 @override 39 40 Widget build(BuildContext context) { 40 41 final typeaheadState = ref.watch(actorTypeaheadProvider); 42 + final l10n = AppLocalizations.of(context); 41 43 42 44 return Padding( 43 45 padding: EdgeInsets.only( ··· 84 86 style: const TextStyle(color: Colors.white), 85 87 cursorColor: Colors.white, 86 88 decoration: InputDecoration( 87 - hintText: 'Search by handle or display name', 89 + hintText: l10n.hintSearchByHandle, 88 90 hintStyle: const TextStyle(color: Color(0xFF64748B)), 89 91 prefixIcon: const Icon(Icons.search, color: Color(0xFF94A3B8)), 90 92 filled: true,
+2 -1
lib/src/core/pro_video_editor/ui/widgets/common/build_stickers.dart
··· 4 4 import 'package:flutter/material.dart'; 5 5 import 'package:pro_image_editor/pro_image_editor.dart'; 6 6 import 'package:spark/src/core/design_system/tokens/colors.dart'; 7 + import 'package:spark/src/core/l10n/app_localizations.dart'; 7 8 8 9 class DemoBuildStickers extends StatefulWidget { 9 10 const DemoBuildStickers({ ··· 47 48 children: [ 48 49 const _DragHandle(), 49 50 _SheetHeader( 50 - title: 'Stickers', 51 + title: AppLocalizations.of(context).labelStickers, 51 52 onClose: () => Navigator.of(context).pop(), 52 53 ), 53 54 Padding(
+2 -1
lib/src/core/pro_video_editor/ui/widgets/common/video_progress_alert.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:pro_video_editor/pro_video_editor.dart'; 3 + import 'package:spark/src/core/l10n/app_localizations.dart'; 3 4 4 5 /// A dialog that displays real-time export progress for video generation. 5 6 /// ··· 68 69 mainAxisSize: MainAxisSize.min, 69 70 crossAxisAlignment: CrossAxisAlignment.start, 70 71 children: [ 71 - const Text('Exporting video…'), 72 + Text(AppLocalizations.of(context).messageExportingVideo), 72 73 const SizedBox(height: 6), 73 74 Text( 74 75 '$percent%',
+15 -9
lib/src/core/pro_video_editor/ui/widgets/layout/video_toolbar.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:spark/src/core/design_system/tokens/colors.dart'; 3 + import 'package:spark/src/core/l10n/app_localizations.dart'; 3 4 4 5 class VideoToolbar extends StatelessWidget { 5 6 const VideoToolbar({ ··· 27 28 28 29 @override 29 30 Widget build(BuildContext context) { 31 + final l10n = AppLocalizations.of(context); 30 32 return ColoredBox( 31 33 color: AppColors.greyBlack, 32 34 child: SafeArea( ··· 38 40 children: [ 39 41 _ToolButton( 40 42 icon: Icons.music_note, 41 - label: 'Sound', 43 + label: l10n.labelSound, 42 44 onPressed: onSound, 43 45 ), 44 46 _ToolButton( 45 47 icon: Icons.brush, 46 - label: 'Paint', 48 + label: l10n.labelPaint, 47 49 onPressed: onPaint, 48 50 ), 49 51 _ToolButton( 50 52 icon: Icons.text_fields, 51 - label: 'Text', 53 + label: l10n.labelText, 52 54 onPressed: onText, 53 55 ), 54 56 _ToolButton( 55 57 icon: Icons.crop_rotate, 56 - label: 'Crop', 58 + label: l10n.labelCrop, 57 59 onPressed: onCrop, 58 60 ), 59 - _ToolButton(icon: Icons.tune, label: 'Tune', onPressed: onTune), 61 + _ToolButton( 62 + icon: Icons.tune, 63 + label: l10n.labelTune, 64 + onPressed: onTune, 65 + ), 60 66 _ToolButton( 61 67 icon: Icons.filter, 62 - label: 'Filter', 68 + label: l10n.labelFilter, 63 69 onPressed: onFilter, 64 70 ), 65 71 _ToolButton( 66 72 icon: Icons.blur_on, 67 - label: 'Blur', 73 + label: l10n.labelBlur, 68 74 onPressed: onBlur, 69 75 ), 70 76 _ToolButton( 71 77 icon: Icons.emoji_emotions, 72 - label: 'Emoji', 78 + label: l10n.labelEmoji, 73 79 onPressed: onEmoji, 74 80 ), 75 81 _ToolButton( 76 82 icon: Icons.star, 77 - label: 'Stickers', 83 + label: l10n.labelStickers, 78 84 onPressed: onStickers, 79 85 ), 80 86 ],
+9 -7
lib/src/core/pro_video_editor/ui/widgets/player/preview_video.dart
··· 4 4 import 'package:flutter/material.dart'; 5 5 import 'package:pro_image_editor/pro_image_editor.dart'; 6 6 import 'package:pro_video_editor/pro_video_editor.dart'; 7 + import 'package:spark/src/core/l10n/app_localizations.dart'; 7 8 import 'package:video_player/video_player.dart'; 8 9 9 10 /// A widget that previews a video from a file path. ··· 71 72 72 73 @override 73 74 Widget build(BuildContext context) { 75 + final l10n = AppLocalizations.of(context); 74 76 return Scaffold( 75 - appBar: AppBar(title: const Text('Result')), 77 + appBar: AppBar(title: Text(l10n.pageTitleResult)), 76 78 body: LayoutBuilder( 77 79 builder: (context, constraints) { 78 80 return Stack( 79 81 children: [ 80 82 Positioned.fill(child: _buildVideoPlayer(constraints)), 81 - _buildGenerationInfos(), 83 + _buildGenerationInfos(l10n), 82 84 ], 83 85 ); 84 86 }, ··· 122 124 ); 123 125 } 124 126 125 - Widget _buildGenerationInfos() { 127 + Widget _buildGenerationInfos(AppLocalizations l10n) { 126 128 const tableSpace = TableRow(children: [SizedBox(height: 3), SizedBox()]); 127 129 return Positioned( 128 130 top: 10, ··· 146 148 children: [ 147 149 TableRow( 148 150 children: [ 149 - const Text('Generation time:'), 151 + Text(l10n.labelGenerationTime), 150 152 Text('${_generationTime}ms', style: _valueStyle), 151 153 ], 152 154 ), ··· 154 156 if (md != null) ...[ 155 157 TableRow( 156 158 children: [ 157 - const Text('Duration:'), 159 + Text(l10n.labelDuration), 158 160 Text(md.duration.toString(), style: _valueStyle), 159 161 ], 160 162 ), 161 163 tableSpace, 162 164 TableRow( 163 165 children: [ 164 - const Text('Size:'), 166 + Text(l10n.labelSize), 165 167 Text(_formatBytes(md.fileSize), style: _valueStyle), 166 168 ], 167 169 ), 168 170 tableSpace, 169 171 TableRow( 170 172 children: [ 171 - const Text('Resolution:'), 173 + Text(l10n.labelResolution), 172 174 Text( 173 175 '${md.resolution.width.toInt()}x' 174 176 '${md.resolution.height.toInt()}',
+2 -1
lib/src/core/ui/widgets/alt_text_editor_dialog.dart
··· 35 35 36 36 @override 37 37 Widget build(BuildContext context) { 38 + final l10n = AppLocalizations.of(context); 38 39 final theme = Theme.of(context); 39 40 final isDarkMode = theme.brightness == Brightness.dark; 40 41 final backgroundColor = isDarkMode ? AppColors.nearBlack : Colors.white; ··· 81 82 maxLines: 4, 82 83 style: TextStyle(color: textColor, fontSize: 16), 83 84 decoration: InputDecoration( 84 - hintText: 'Add alt text', 85 + hintText: l10n.hintAddAltText, 85 86 hintStyle: TextStyle(color: textColor.withAlpha(100)), 86 87 border: InputBorder.none, 87 88 contentPadding: const EdgeInsets.symmetric(
+2 -1
lib/src/core/ui/widgets/custom_text_field.dart
··· 1 1 import 'package:flutter/material.dart'; 2 + import 'package:spark/src/core/l10n/app_localizations.dart'; 2 3 3 4 /// A custom styled text field with optional undo functionality. 4 5 class CustomTextField extends StatelessWidget { ··· 55 56 ? IconButton( 56 57 icon: const Icon(Icons.undo, size: 20), 57 58 onPressed: onUndo, 58 - tooltip: 'Revert', 59 + tooltip: AppLocalizations.of(context).tooltipRevert, 59 60 ) 60 61 : null, 61 62 ),
+4 -2
lib/src/core/ui/widgets/report_dialog.dart
··· 6 6 import 'package:flutter/material.dart'; 7 7 import 'package:flutter_riverpod/flutter_riverpod.dart'; 8 8 import 'package:get_it/get_it.dart'; 9 + import 'package:spark/src/core/l10n/app_localizations.dart'; 9 10 import 'package:spark/src/core/design_system/components/atoms/buttons/long_button.dart'; 10 11 import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; 11 12 import 'package:spark/src/core/utils/logging/log_service.dart'; ··· 428 429 429 430 @override 430 431 Widget build(BuildContext context) { 432 + final l10n = AppLocalizations.of(context); 431 433 final theme = Theme.of(context); 432 434 final textColor = 433 435 theme.textTheme.bodyMedium?.color ?? ··· 507 509 maxLines: 3, 508 510 style: theme.textTheme.bodySmall?.copyWith(color: textColor), 509 511 decoration: InputDecoration( 510 - hintText: 'Additional details (optional)', 512 + hintText: l10n.hintAdditionalDetails, 511 513 contentPadding: const EdgeInsets.symmetric( 512 514 horizontal: 8, 513 515 vertical: 8, ··· 552 554 height: 16, 553 555 child: CircularProgressIndicator(strokeWidth: 2), 554 556 ) 555 - : LongButton(label: 'Submit', onPressed: _submitReport), 557 + : LongButton(label: l10n.buttonSubmit, onPressed: _submitReport), 556 558 ], 557 559 ); 558 560 }
+8 -6
lib/src/features/auth/ui/pages/login_page.dart
··· 7 7 import 'package:spark/src/core/design_system/components/atoms/buttons/app_overlay_back_button.dart'; 8 8 import 'package:spark/src/core/design_system/components/atoms/buttons/long_button.dart'; 9 9 import 'package:spark/src/core/design_system/tokens/typography.dart'; 10 + import 'package:spark/src/core/l10n/app_localizations.dart'; 10 11 import 'package:spark/src/core/routing/app_router.dart'; 11 12 import 'package:spark/src/features/auth/providers/auth_providers.dart'; 12 13 import 'package:spark/src/features/auth/providers/onboarding_providers.dart'; ··· 132 133 133 134 @override 134 135 Widget build(BuildContext context) { 136 + final l10n = AppLocalizations.of(context); 135 137 final isLoading = ref.watch( 136 138 authProvider.select((state) => state.isLoading), 137 139 ); ··· 155 157 children: [ 156 158 if (!_hasReceivedCallback) ...[ 157 159 Text( 158 - 'Sign In', 160 + l10n.pageTitleSignIn, 159 161 style: AppTypography.displaySmallBold.copyWith( 160 162 color: colorScheme.onSurface, 161 163 ), ··· 163 165 ), 164 166 const SizedBox(height: 8), 165 167 Text( 166 - 'Enter your handle to continue with OAuth', 168 + l10n.messageEnterHandle, 167 169 style: AppTypography.textMediumMedium.copyWith( 168 170 color: colorScheme.onSurfaceVariant, 169 171 ), ··· 231 233 switch (error) { 232 234 final String e 233 235 when e.contains('must be a valid handle') => 234 - 'Invalid handle', 236 + l10n.errorInvalidHandle, 235 237 final String e 236 238 when e.contains('Failed to resolve') => 237 - 'Could not find this handle', 239 + l10n.errorHandleNotFound, 238 240 _ => error, 239 241 }, 240 242 style: AppTypography.textSmallMedium.copyWith( ··· 250 252 const CircularProgressIndicator(), 251 253 const SizedBox(height: 16), 252 254 Text( 253 - 'Completing sign in...', 255 + l10n.errorCompletingSignIn, 254 256 style: AppTypography.textMediumMedium.copyWith( 255 257 color: colorScheme.onSurfaceVariant, 256 258 ), ··· 262 264 Opacity( 263 265 opacity: isLoading ? 0.5 : 1.0, 264 266 child: LongButton( 265 - label: 'Continue', 267 + label: l10n.buttonContinue, 266 268 onPressed: isLoading ? null : _initiateOAuth, 267 269 ), 268 270 ),
+7 -6
lib/src/features/auth/ui/pages/register_page.dart
··· 6 6 import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; 7 7 import 'package:spark/src/core/design_system/components/atoms/buttons/long_button.dart'; 8 8 import 'package:spark/src/core/design_system/tokens/typography.dart'; 9 + import 'package:spark/src/core/l10n/app_localizations.dart'; 9 10 import 'package:spark/src/core/routing/app_router.dart'; 10 11 import 'package:spark/src/features/auth/providers/auth_providers.dart'; 11 12 import 'package:spark/src/features/auth/providers/onboarding_providers.dart'; ··· 111 112 112 113 @override 113 114 Widget build(BuildContext context) { 115 + final l10n = AppLocalizations.of(context); 114 116 final isLoading = ref.watch( 115 117 authProvider.select((state) => state.isLoading), 116 118 ); ··· 136 138 const CircularProgressIndicator(), 137 139 const SizedBox(height: 16), 138 140 Text( 139 - 'Completing sign up...', 141 + l10n.messageCompletingSignUp, 140 142 style: AppTypography.textMediumMedium.copyWith( 141 143 color: colorScheme.onSurfaceVariant, 142 144 ), ··· 163 165 const SizedBox(height: 32), 164 166 // Welcome text 165 167 Text( 166 - 'Welcome!', 168 + l10n.messageWelcome, 167 169 style: AppTypography.displaySmallBold.copyWith( 168 170 color: colorScheme.onSurface, 169 171 ), ··· 171 173 ), 172 174 const SizedBox(height: 12), 173 175 Text( 174 - 'Share videos, connect with friends,\n' 175 - 'and take back your timeline.', 176 + l10n.messageWelcomeDescription, 176 177 style: AppTypography.textMediumMedium.copyWith( 177 178 color: colorScheme.onSurfaceVariant, 178 179 height: 1.5, ··· 198 199 Opacity( 199 200 opacity: isLoading ? 0.5 : 1.0, 200 201 child: LongButton( 201 - label: 'Get Started', 202 + label: l10n.buttonGetStarted, 202 203 onPressed: isLoading ? null : _initiateOAuth, 203 204 ), 204 205 ), 205 206 const SizedBox(height: 12), 206 207 LongButton( 207 - label: 'I already have an account', 208 + label: l10n.buttonHaveAccount, 208 209 variant: LongButtonVariant.regular, 209 210 onPressed: () => context.router.push(const LoginRoute()), 210 211 ),
+4 -4
lib/src/features/comments/ui/pages/crosspost_comments_page.dart
··· 4 4 import 'package:flutter/material.dart'; 5 5 import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 6 import 'package:get_it/get_it.dart'; 7 + import 'package:spark/src/core/l10n/app_localizations.dart'; 7 8 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 8 9 import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; 9 10 import 'package:spark/src/core/ui/widgets/image_content.dart'; ··· 31 32 @override 32 33 Widget build(BuildContext context, WidgetRef ref) { 33 34 final anchorUri = AtUri.parse(postUri); 35 + final l10n = AppLocalizations.of(context); 34 36 final asyncComments = ref.watch(crosspostCommentsProvider(anchorUri)); 35 37 final textColor = Theme.of(context).colorScheme.onSurface; 36 38 final borderColor = Theme.of(context).colorScheme.outline; ··· 76 78 child: asyncComments.when( 77 79 data: (comments) { 78 80 if (comments.isEmpty) { 79 - return const Center( 80 - child: Text('No crosspost comments yet.'), 81 - ); 81 + return Center(child: Text(l10n.emptyNoCrosspostComments)); 82 82 } 83 83 84 84 return ListView.separated( ··· 97 97 }, 98 98 loading: () => const Center(child: CircularProgressIndicator()), 99 99 error: (error, stackTrace) => 100 - Center(child: Text('Error: $error')), 100 + Center(child: Text(l10n.errorWithDetail(error.toString()))), 101 101 ), 102 102 ), 103 103 ],
+5 -2
lib/src/features/comments/ui/pages/replies_page.dart
··· 2 2 import 'package:auto_route/auto_route.dart'; 3 3 import 'package:flutter/material.dart'; 4 4 import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 + import 'package:spark/src/core/l10n/app_localizations.dart'; 5 6 import 'package:spark/src/core/design_system/components/atoms/buttons/app_leading_button.dart'; 6 7 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 7 8 import 'package:spark/src/features/comments/providers/comments_page_provider.dart'; ··· 59 60 60 61 @override 61 62 Widget build(BuildContext context) { 63 + final l10n = AppLocalizations.of(context); 62 64 final state = ref.watch( 63 65 commentsPageProvider(postUri: AtUri.parse(widget.postUri)), 64 66 ); ··· 72 74 backgroundColor: backgroundColor, 73 75 elevation: 0, 74 76 title: Text( 75 - 'Replies', 77 + l10n.pageTitleReplies, 76 78 style: TextStyle(color: textColor, fontWeight: FontWeight.bold), 77 79 ), 78 80 leading: AppLeadingButton(color: textColor), ··· 118 120 ], 119 121 ), 120 122 ), 121 - error: (error, stackTrace) => Center(child: Text('Error: $error')), 123 + error: (error, stackTrace) => 124 + Center(child: Text(l10n.errorWithDetail(error.toString()))), 122 125 loading: () => const Center(child: CircularProgressIndicator()), 123 126 ), 124 127 );
+8 -3
lib/src/features/comments/ui/widgets/comment_input.dart
··· 6 6 import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 7 import 'package:image_picker/image_picker.dart'; 8 8 import 'package:spark/src/core/design_system/tokens/colors.dart'; 9 + import 'package:spark/src/core/l10n/app_localizations.dart'; 9 10 import 'package:spark/src/core/design_system/tokens/constants.dart'; 10 11 import 'package:spark/src/core/design_system/tokens/typography.dart'; 11 12 import 'package:spark/src/core/ui/widgets/alt_text_editor_dialog.dart'; ··· 48 49 49 50 @override 50 51 Widget build(BuildContext context) { 52 + final l10n = AppLocalizations.of(context); 51 53 final state = ref.watch(commentInputProvider(imagePicker)); 52 54 final notifier = ref.read(commentInputProvider(imagePicker).notifier); 53 55 final authState = ref.watch(authProvider); ··· 96 98 child: MentionInputField( 97 99 controller: state.mentionController, 98 100 onMentionsChanged: (_) {}, 99 - hintText: 'Add a comment...', 101 + hintText: l10n.hintAddComment, 100 102 maxChars: _maxChars, 101 103 maxLines: 5, 102 104 minLines: 1, ··· 253 255 254 256 @override 255 257 Widget build(BuildContext context) { 258 + final l10n = AppLocalizations.of(context); 256 259 final canAddMoreImages = state.selectedImages.isEmpty; 257 260 final enabled = !state.isPosting && canAddMoreImages; 258 261 ··· 262 265 constraints: const BoxConstraints(minWidth: 16, minHeight: 16), 263 266 onPressed: enabled ? () => notifier.pickImages(context) : null, 264 267 tooltip: enabled 265 - ? 'Add image (1 max)' 266 - : (state.isPosting ? 'Posting...' : 'Maximum images reached'), 268 + ? l10n.hintAddImage 269 + : (state.isPosting 270 + ? l10n.messagePostingStory 271 + : l10n.messageMaximumImagesReached), 267 272 icon: Icon( 268 273 FluentIcons.image_24_regular, 269 274 size: 24,
+3 -5
lib/src/features/comments/ui/widgets/comment_item.dart
··· 86 86 context: context, 87 87 builder: (context) => AlertDialog( 88 88 title: Text(l10n.dialogDeleteComment), 89 - content: Text( 90 - 'Are you sure you want to delete this comment? This action ' 91 - 'cannot be undone.', 92 - ), 89 + content: Text(l10n.dialogDeleteCommentConfirm), 93 90 actions: [ 94 91 TextButton( 95 92 onPressed: () => context.router.maybePop(), ··· 330 327 331 328 @override 332 329 Widget build(BuildContext context) { 330 + final l10n = AppLocalizations.of(context); 333 331 final notifier = ref.read(commentProvider(widget.thread).notifier); 334 332 return Row( 335 333 children: [ ··· 369 367 ); 370 368 }, 371 369 child: Text( 372 - 'Reply', 370 + l10n.labelReply, 373 371 style: TextStyle(fontSize: 12, color: secondaryTextColor), 374 372 ), 375 373 ),
+4 -2
lib/src/features/feed/ui/pages/feed_page.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 + import 'package:spark/src/core/l10n/app_localizations.dart'; 3 4 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 4 5 import 'package:spark/src/core/ui/foundation/colors.dart'; 5 6 import 'package:spark/src/features/feed/providers/feed_action_controller.dart'; ··· 87 88 Widget build(BuildContext context) { 88 89 super.build(context); // Required for AutomaticKeepAliveClientMixin 89 90 91 + final l10n = AppLocalizations.of(context); 90 92 final state = ref.watch(feedProvider(widget.feed)); 91 93 final notifier = ref.read(feedProvider(widget.feed).notifier); 92 94 final shouldBeActive = ref.watch( ··· 156 158 child: Column( 157 159 mainAxisAlignment: MainAxisAlignment.center, 158 160 children: [ 159 - const Text('Error loading feed'), 161 + Text(l10n.errorLoadingFeed), 160 162 TextButton( 161 163 onPressed: onRefresh, 162 - child: const Text('Try again'), 164 + child: Text(l10n.buttonTryAgain), 163 165 ), 164 166 ], 165 167 ),
+3 -1
lib/src/features/feed/ui/pages/standalone_post_page.dart
··· 7 7 import 'package:get_it/get_it.dart'; 8 8 import 'package:spark/src/core/design_system/components/atoms/buttons/app_overlay_back_button.dart'; 9 9 import 'package:spark/src/core/design_system/tokens/constants.dart'; 10 + import 'package:spark/src/core/l10n/app_localizations.dart'; 10 11 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 11 12 import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; 12 13 import 'package:spark/src/core/routing/app_router.dart'; ··· 140 141 141 142 @override 142 143 Widget build(BuildContext context) { 144 + final l10n = AppLocalizations.of(context); 143 145 // Watch for post updates to trigger reload 144 146 final updateCount = ref.watch(postUpdateProvider(widget.postUri)); 145 147 ··· 251 253 const Icon(Icons.error, color: Colors.white, size: 48), 252 254 const SizedBox(height: 16), 253 255 Text( 254 - 'Error loading post: ${snapshot.error}', 256 + l10n.errorWithDetail(snapshot.error.toString()), 255 257 style: const TextStyle(color: Colors.white), 256 258 textAlign: TextAlign.center, 257 259 ),
+12 -6
lib/src/features/feed/ui/widgets/action_buttons/share_panel.dart
··· 6 6 import 'package:share_plus/share_plus.dart'; 7 7 import 'package:skeletonizer/skeletonizer.dart'; 8 8 import 'package:spark/src/core/design_system/components/atoms/buttons/long_button.dart'; 9 + import 'package:spark/src/core/l10n/app_localizations.dart'; 9 10 import 'package:spark/src/core/utils/logging/log_service.dart'; 10 11 import 'package:spark/src/features/messages/providers/conversation_provider.dart'; 11 12 import 'package:spark/src/features/messages/providers/conversations_provider.dart'; ··· 86 87 87 88 @override 88 89 Widget build(BuildContext context) { 90 + final l10n = AppLocalizations.of(context); 89 91 final theme = Theme.of(context); 90 92 final textColor = theme.colorScheme.onSurface; 91 93 final dividerColor = theme.colorScheme.outline.withValues(alpha: 0.2); ··· 124 126 Padding( 125 127 padding: const EdgeInsets.symmetric(horizontal: 20), 126 128 child: Text( 127 - 'Share', 129 + l10n.labelShare, 128 130 style: TextStyle( 129 131 color: textColor, 130 132 fontSize: 18, ··· 142 144 return Padding( 143 145 padding: const EdgeInsets.fromLTRB(20, 8, 20, 0), 144 146 child: Text( 145 - 'No conversations yet', 147 + l10n.emptyNoConversations, 146 148 style: TextStyle(color: textColor.withAlpha(153)), 147 149 ), 148 150 ); ··· 172 174 error: (e, st) => Padding( 173 175 padding: const EdgeInsets.fromLTRB(20, 8, 20, 0), 174 176 child: Text( 175 - 'Failed to load conversations', 177 + l10n.errorLoadingConversations, 176 178 style: TextStyle(color: theme.colorScheme.error), 177 179 ), 178 180 ), ··· 217 219 key: const ValueKey('selected-actions'), 218 220 width: double.infinity, 219 221 child: LongButton( 220 - label: _sending ? 'Sending...' : 'Send', 222 + label: _sending 223 + ? l10n.messageSending 224 + : l10n.buttonSend, 221 225 onPressed: _sending 222 226 ? null 223 227 : _sendToSelectedConversation, ··· 228 232 children: [ 229 233 Expanded( 230 234 child: LongButton( 231 - label: _copiedLink ? 'Copied' : 'Copy link', 235 + label: _copiedLink 236 + ? l10n.buttonCopied 237 + : l10n.buttonCopyLink, 232 238 onPressed: _copyLink, 233 239 variant: LongButtonVariant.regular, 234 240 ), ··· 236 242 const SizedBox(width: 12), 237 243 Expanded( 238 244 child: LongButton( 239 - label: 'Share', 245 + label: l10n.buttonShare, 240 246 onPressed: _shareNatively, 241 247 variant: LongButtonVariant.regular, 242 248 ),
+12 -13
lib/src/features/feed/ui/widgets/action_buttons/side_action_bar.dart
··· 7 7 import 'package:share_plus/share_plus.dart'; 8 8 import 'package:spark/src/core/auth/data/repositories/auth_repository.dart'; 9 9 import 'package:spark/src/core/design_system/components/organisms/side_action_bar.dart'; 10 + import 'package:spark/src/core/l10n/app_localizations.dart'; 10 11 import 'package:spark/src/core/network/atproto/atproto.dart'; 11 12 import 'package:spark/src/core/routing/app_router.dart'; 12 13 import 'package:spark/src/core/ui/widgets/options_panel.dart'; ··· 313 314 314 315 Future<void> _handleDeletePost() async { 315 316 final currentPost = _currentPost ?? widget.post; 317 + final l10n = AppLocalizations.of(context); 316 318 final confirmed = await showDialog<bool>( 317 319 context: context, 318 320 builder: (context) => AlertDialog( 319 - title: const Text('Delete Post'), 320 - content: const Text( 321 - 'Are you sure you want to delete this post?' 322 - '\nThis action cannot be undone.', 323 - ), 321 + title: Text(l10n.dialogDeletePost), 322 + content: Text(l10n.dialogDeletePostConfirm), 324 323 actions: [ 325 324 TextButton( 326 325 onPressed: () => Navigator.of(context).pop(false), 327 - child: const Text('Cancel'), 326 + child: Text(l10n.buttonCancel), 328 327 ), 329 328 TextButton( 330 329 onPressed: () => Navigator.of(context).pop(true), 331 - child: const Text('Delete'), 330 + child: Text(l10n.buttonDelete), 332 331 ), 333 332 ], 334 333 ), ··· 359 358 final currentPost = _currentPost ?? widget.post; 360 359 final author = currentPost.author; 361 360 final wasBlocked = isBlocking(author.viewer); 361 + final l10n = AppLocalizations.of(context); 362 362 363 363 // Show confirmation dialog 364 364 final confirmed = await showDialog<bool>( 365 365 context: context, 366 366 builder: (context) => AlertDialog( 367 - title: Text(wasBlocked ? 'Unblock User' : 'Block User'), 367 + title: Text(wasBlocked ? l10n.dialogUnblockUser : l10n.dialogBlockUser), 368 368 content: Text( 369 369 wasBlocked 370 - ? 'Are you sure you want to unblock this user?' 371 - : 'Are you sure you want to block this user? ' 372 - 'You will no longer see their posts.', 370 + ? l10n.dialogUnblockUserConfirm 371 + : l10n.dialogBlockUserConfirm, 373 372 ), 374 373 actions: [ 375 374 TextButton( 376 375 onPressed: () => Navigator.of(context).pop(false), 377 - child: const Text('Cancel'), 376 + child: Text(l10n.buttonCancel), 378 377 ), 379 378 TextButton( 380 379 onPressed: () => Navigator.of(context).pop(true), 381 380 style: TextButton.styleFrom( 382 381 foregroundColor: wasBlocked ? null : Colors.red, 383 382 ), 384 - child: Text(wasBlocked ? 'Unblock' : 'Block'), 383 + child: Text(wasBlocked ? l10n.buttonUnblock : l10n.buttonBlock), 385 384 ), 386 385 ], 387 386 ),
+18 -12
lib/src/features/feed/ui/widgets/feed/feeds_bar.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 3 import 'package:spark/src/core/design_system/components/molecules/create_media_sheet.dart'; 4 + import 'package:spark/src/core/l10n/app_localizations.dart'; 4 5 import 'package:spark/src/core/design_system/components/molecules/feed_tag_list.dart'; 5 6 import 'package:spark/src/core/design_system/templates/feeds_bar_template.dart'; 6 7 import 'package:spark/src/core/media/create_media_actions.dart'; ··· 53 54 borderRadius: BorderRadius.vertical(top: Radius.circular(16)), 54 55 ), 55 56 builder: (context) { 57 + final l10n = AppLocalizations.of(context); 56 58 return SafeArea( 57 59 child: Padding( 58 60 padding: const EdgeInsets.symmetric(vertical: 8), ··· 78 80 vertical: 8, 79 81 ), 80 82 child: Text( 81 - feed.view?.displayName ?? 'Following', 83 + feed.view?.displayName ?? l10n.labelFollowing, 82 84 style: Theme.of(context).textTheme.titleMedium?.copyWith( 83 85 fontWeight: FontWeight.bold, 84 86 ), ··· 92 94 isLiked ? Icons.favorite : Icons.favorite_border, 93 95 color: isLiked ? Colors.red : null, 94 96 ), 95 - title: Text(isLiked ? 'Unlike Feed' : 'Like Feed'), 97 + title: Text( 98 + isLiked ? l10n.buttonUnlikeFeed : l10n.buttonLikeFeed, 99 + ), 96 100 onTap: () async { 97 101 Navigator.pop(context); 98 102 if (isLiked) { ··· 113 117 Icons.delete_outline, 114 118 color: Colors.red, 115 119 ), 116 - title: const Text( 117 - 'Remove Feed', 118 - style: TextStyle(color: Colors.red), 120 + title: Text( 121 + l10n.dialogRemoveFeed, 122 + style: const TextStyle(color: Colors.red), 119 123 ), 120 124 onTap: () async { 121 125 Navigator.pop(context); ··· 123 127 final confirmed = await showDialog<bool>( 124 128 context: context, 125 129 builder: (context) => AlertDialog( 126 - title: const Text('Remove Feed'), 130 + title: Text(l10n.dialogRemoveFeed), 127 131 content: Text( 128 - 'Are you sure you want to remove ' 129 - '"${feed.view?.displayName ?? 'this feed'}"?', 132 + l10n.dialogRemoveFeedConfirm( 133 + feed.view?.displayName ?? l10n.labelFollowing, 134 + ), 130 135 ), 131 136 actions: [ 132 137 TextButton( 133 138 onPressed: () => Navigator.pop(context, false), 134 - child: const Text('Cancel'), 139 + child: Text(l10n.buttonCancel), 135 140 ), 136 141 TextButton( 137 142 onPressed: () => Navigator.pop(context, true), 138 143 style: TextButton.styleFrom( 139 144 foregroundColor: Colors.red, 140 145 ), 141 - child: const Text('Remove'), 146 + child: Text(l10n.buttonRemove), 142 147 ), 143 148 ], 144 149 ), ··· 153 158 // Cancel option 154 159 ListTile( 155 160 leading: const Icon(Icons.close), 156 - title: const Text('Cancel'), 161 + title: Text(l10n.buttonCancel), 157 162 onTap: () => Navigator.pop(context), 158 163 ), 159 164 ], ··· 166 171 167 172 @override 168 173 Widget build(BuildContext context) { 174 + final l10n = AppLocalizations.of(context); 169 175 final settings = ref.watch(settingsProvider); 170 176 171 177 // Only show pinned feeds in the home view ··· 178 184 feed.type == 'timeline' && feed.config.value == 'following'; 179 185 return FeedTagData( 180 186 id: feed.config.id, 181 - text: feed.view != null ? feed.view!.displayName : 'Following', 187 + text: feed.view != null ? feed.view!.displayName : l10n.labelFollowing, 182 188 isTimeline: isTimeline, 183 189 isLiked: feed.view?.viewer?.like != null, 184 190 canDelete: !isTimeline,
+3 -1
lib/src/features/feed/ui/widgets/post/alt_text_dialog.dart
··· 1 1 import 'package:flutter/material.dart'; 2 + import 'package:spark/src/core/l10n/app_localizations.dart'; 2 3 3 4 /// A dialog that displays the alt text (image description) for an image. 4 5 class AltTextDialog extends StatelessWidget { ··· 8 9 @override 9 10 Widget build(BuildContext context) { 10 11 final theme = Theme.of(context); 12 + final l10n = AppLocalizations.of(context); 11 13 12 14 return SafeArea( 13 15 left: false, ··· 40 42 ), 41 43 ), 42 44 ), 43 - Text('Image Description', style: theme.textTheme.titleLarge), 45 + Text(l10n.hintImageDescription, style: theme.textTheme.titleLarge), 44 46 const SizedBox(height: 12), 45 47 Text( 46 48 altText,
+3 -1
lib/src/features/feed/ui/widgets/post/feed_post_widget.dart
··· 4 4 import 'package:flutter/services.dart'; 5 5 import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 6 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 7 + import 'package:spark/src/core/l10n/app_localizations.dart'; 7 8 import 'package:spark/src/core/routing/app_router.dart'; 8 9 import 'package:spark/src/core/ui/foundation/colors.dart'; 9 10 import 'package:spark/src/core/ui/widgets/content_warning_overlay.dart'; ··· 152 153 153 154 @override 154 155 Widget build(BuildContext context) { 156 + final l10n = AppLocalizations.of(context); 155 157 // Check if we need to reload post due to state changes 156 158 final feedState = ref.watch(feedProvider(widget.feed)); 157 159 final navigationState = ref.watch(navigationProvider); ··· 345 347 decoration: const BoxDecoration(color: AppColors.black), 346 348 child: Center( 347 349 child: Text( 348 - 'Error loading post: ${snapshot.error}', 350 + l10n.errorWithDetail(snapshot.error.toString()), 349 351 style: const TextStyle(color: Colors.white), 350 352 ), 351 353 ),
+4 -2
lib/src/features/messages/ui/pages/chat_page.dart
··· 9 9 import 'package:spark/src/features/auth/providers/auth_providers.dart'; 10 10 import 'package:spark/src/features/messages/providers/conversation_provider.dart'; 11 11 import 'package:spark/src/features/messages/providers/polling_timer.dart'; 12 + import 'package:spark/src/core/l10n/app_localizations.dart'; 12 13 import 'package:spark/src/features/messages/ui/widgets/messages_list.dart'; 13 14 14 15 @RoutePage() ··· 67 68 68 69 @override 69 70 Widget build(BuildContext context) { 71 + final l10n = AppLocalizations.of(context); 70 72 final state = ref.watch(conversationProvider(widget.conversationId)); 71 73 ref 72 74 ..listen( ··· 109 111 ), 110 112 const SizedBox(height: 16), 111 113 Text( 112 - 'Failed to load messages', 114 + l10n.errorLoadingMessages, 113 115 style: TextStyle(color: Theme.of(context).colorScheme.error), 114 116 ), 115 117 const SizedBox(height: 8), 116 118 ElevatedButton( 117 119 onPressed: () => 118 120 ref.invalidate(conversationProvider(widget.conversationId)), 119 - child: const Text('Retry'), 121 + child: Text(l10n.buttonRetry), 120 122 ), 121 123 ], 122 124 ),
+4 -2
lib/src/features/messages/ui/pages/messages_page.dart
··· 6 6 import 'package:spark/src/core/design_system/templates/chat_list_page_template.dart'; 7 7 import 'package:spark/src/core/routing/app_router.dart'; 8 8 import 'package:spark/src/core/utils/logging/logging.dart'; 9 + import 'package:spark/src/core/l10n/app_localizations.dart'; 9 10 import 'package:spark/src/features/messages/providers/conversations_provider.dart'; 10 11 11 12 @RoutePage() ··· 19 20 class _MessagesPageState extends ConsumerState<MessagesPage> { 20 21 @override 21 22 Widget build(BuildContext context) { 23 + final l10n = AppLocalizations.of(context); 22 24 final logger = GetIt.instance<LogService>().getLogger('MessagesPage'); 23 25 final chatServiceState = ref.watch(conversationsProvider); 24 26 ··· 89 91 ), 90 92 const SizedBox(height: 16), 91 93 Text( 92 - 'Failed to load conversations', 94 + l10n.errorLoadingConversations, 93 95 style: TextStyle(color: theme.colorScheme.error), 94 96 ), 95 97 const SizedBox(height: 8), 96 98 ElevatedButton( 97 99 onPressed: () => ref.invalidate(conversationsProvider), 98 - child: const Text('Retry'), 100 + child: Text(l10n.buttonRetry), 99 101 ), 100 102 ], 101 103 ),
+5 -3
lib/src/features/messages/ui/pages/new_chat_search_page.dart
··· 8 8 import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; 9 9 import 'package:spark/src/core/network/messages/data/repository/messages_repository.dart'; 10 10 import 'package:spark/src/core/routing/app_router.dart'; 11 + import 'package:spark/src/core/l10n/app_localizations.dart'; 11 12 import 'package:spark/src/features/search/providers/search_provider.dart'; 12 13 13 14 @RoutePage() ··· 43 44 44 45 @override 45 46 Widget build(BuildContext context) { 47 + final l10n = AppLocalizations.of(context); 46 48 final searchState = ref.watch(searchProvider); 47 49 final theme = Theme.of(context); 48 50 final colorScheme = theme.colorScheme; ··· 59 61 padding: const EdgeInsets.all(16), 60 62 child: Row( 61 63 children: [ 62 - const AppLeadingButton(tooltip: 'Back'), 64 + AppLeadingButton(tooltip: l10n.tooltipBack), 63 65 const SizedBox(width: 8), 64 66 Expanded( 65 67 child: TextField( 66 68 controller: _searchController, 67 69 decoration: InputDecoration( 68 - hintText: 'Search users', 70 + hintText: l10n.hintSearchUsers, 69 71 prefixIcon: const Icon(FluentIcons.search_24_regular), 70 72 suffixIcon: _searchController.text.isNotEmpty 71 73 ? IconButton( ··· 131 133 ), 132 134 ), 133 135 child: TabBar( 134 - tabs: const [Tab(text: 'Users')], 136 + tabs: [Tab(text: l10n.tabUsers)], 135 137 indicatorColor: colorScheme.primary, 136 138 labelColor: theme.textTheme.bodyLarge?.color, 137 139 unselectedLabelColor: theme.textTheme.bodyMedium?.color,
+3 -1
lib/src/features/messages/ui/widgets/message_input.dart
··· 5 5 import 'package:image_picker/image_picker.dart'; 6 6 import 'package:spark/src/core/ui/foundation/colors.dart'; 7 7 import 'package:spark/src/core/ui/widgets/user_avatar.dart'; 8 + import 'package:spark/src/core/l10n/app_localizations.dart'; 8 9 import 'package:spark/src/features/auth/providers/auth_providers.dart'; 9 10 import 'package:spark/src/features/profile/providers/profile_provider.dart'; 10 11 ··· 26 27 27 28 @override 28 29 Widget build(BuildContext context, WidgetRef ref) { 30 + final l10n = AppLocalizations.of(context); 29 31 final authState = ref.watch(authProvider); 30 32 final userDid = authState.did ?? ''; 31 33 final userHandle = authState.handle ?? ''; ··· 70 72 child: TextField( 71 73 controller: controller, 72 74 decoration: InputDecoration( 73 - hintText: 'Type a message...', 75 + hintText: l10n.hintTypeMessage, 74 76 hintStyle: TextStyle( 75 77 color: Theme.of(context).colorScheme.onSurface, 76 78 ),
+6 -4
lib/src/features/notifications/ui/widgets/notifications_list.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 3 import 'package:spark/src/features/notifications/providers/notification_provider.dart'; 4 + import 'package:spark/src/core/l10n/app_localizations.dart'; 4 5 import 'package:spark/src/features/notifications/ui/widgets/notification_item.dart'; 5 6 6 7 class NotificationsList extends ConsumerStatefulWidget { ··· 47 48 48 49 @override 49 50 Widget build(BuildContext context) { 51 + final l10n = AppLocalizations.of(context); 50 52 final notificationState = ref.watch( 51 53 notificationProvider(priority: widget.priority, reasons: widget.reasons), 52 54 ); ··· 89 91 ), 90 92 const SizedBox(height: 16), 91 93 Text( 92 - 'Failed to load notifications', 94 + l10n.errorLoadingNotifications, 93 95 style: TextStyle( 94 96 color: colorScheme.onSurface.withAlpha(179), 95 97 fontSize: 16, ··· 121 123 reasons: widget.reasons, 122 124 ); 123 125 }, 124 - child: const Text('Retry'), 126 + child: Text(l10n.buttonRetry), 125 127 ), 126 128 ], 127 129 ), ··· 160 162 ), 161 163 const SizedBox(height: 16), 162 164 Text( 163 - 'No notifications', 165 + l10n.emptyNoNotifications, 164 166 style: TextStyle( 165 167 color: colorScheme.onSurface.withAlpha(179), 166 168 fontSize: 18, ··· 169 171 ), 170 172 const SizedBox(height: 8), 171 173 Text( 172 - "You're all caught up!", 174 + l10n.messageAllCaughtUp, 173 175 style: TextStyle( 174 176 color: colorScheme.onSurface.withAlpha(102), 175 177 fontSize: 14,
+4 -2
lib/src/features/posting/ui/pages/image_review_page.dart
··· 5 5 import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 6 import 'package:get_it/get_it.dart'; 7 7 import 'package:image_picker/image_picker.dart'; 8 + import 'package:spark/src/core/l10n/app_localizations.dart'; 8 9 import 'package:spark/src/core/design_system/templates/image_review_page_template.dart'; 9 10 import 'package:spark/src/core/design_system/tokens/constants.dart'; 10 11 import 'package:spark/src/core/network/atproto/atproto.dart'; ··· 158 159 159 160 @override 160 161 Widget build(BuildContext context) { 162 + final l10n = AppLocalizations.of(context); 161 163 final canPickMore = _imageFiles.length < _maxImages; 162 164 final showCrossPostWarning = _crosspostToBsky && _imageFiles.length > 4; 163 165 final textLength = _descriptionController.text.runes.length; 164 166 final isOverLimit = textLength > AppConstants.postDescriptionMaxChars; 165 167 166 168 return ImageReviewPageTemplate( 167 - title: 'Review Image Post', 169 + title: l10n.pageTitleReviewImagePost, 168 170 onBack: () => context.router.maybePop(), 169 171 imagePaths: _imageFiles.map((e) => e.path).toList(), 170 172 currentPage: _currentPage, ··· 193 195 crossPostValue: _crosspostToBsky, 194 196 onCrossPostChanged: (v) => setState(() => _crosspostToBsky = v), 195 197 showCrossPostWarning: showCrossPostWarning, 196 - postLabel: 'Post', 198 + postLabel: l10n.buttonPost, 197 199 isPosting: _isPosting, 198 200 isOverLimit: isOverLimit, 199 201 onPost: _isPosting
+36 -21
lib/src/features/posting/ui/pages/media_picker_page.dart
··· 4 4 import 'package:get_it/get_it.dart'; 5 5 import 'package:image_picker/image_picker.dart'; 6 6 import 'package:photo_manager/photo_manager.dart'; 7 + import 'package:spark/src/core/l10n/app_localizations.dart'; 7 8 import 'package:spark/src/core/utils/logging/logging.dart'; 8 9 import 'package:spark/src/features/posting/ui/models/media_selection.dart'; 9 10 ··· 207 208 208 209 void _toggleMultiPhotoSelection(AssetEntity asset) { 209 210 if (asset.type != AssetType.image) { 210 - _showSnackBar('You can only select photos in multi-select mode.'); 211 + _showSnackBar(AppLocalizations.of(context).errorPhotoSelectLimit); 211 212 return; 212 213 } 213 214 ··· 223 224 } 224 225 225 226 if (_selectedPhotoAssets.length >= widget.maxMultiPhotoSelection) { 226 - _showSnackBar('You can select up to ${widget.maxMultiPhotoSelection}.'); 227 + _showSnackBar( 228 + AppLocalizations.of( 229 + context, 230 + ).errorPhotoSelectMax(widget.maxMultiPhotoSelection), 231 + ); 227 232 return; 228 233 } 229 234 ··· 235 240 Future<void> _submitMultiPhotoSelection() async { 236 241 if (_selectedPhotoAssets.isEmpty) return; 237 242 243 + final errorUnableToAccessPhotos = AppLocalizations.of( 244 + context, 245 + ).errorUnableToAccessPhotos; 238 246 final files = <XFile>[]; 239 247 for (final asset in _selectedPhotoAssets) { 240 248 final file = await _assetToXFile(asset, showErrorMessage: false); ··· 244 252 } 245 253 246 254 if (files.isEmpty) { 247 - _showSnackBar('Unable to access the selected photos.'); 255 + _showSnackBar(errorUnableToAccessPhotos); 248 256 return; 249 257 } 250 258 ··· 256 264 AssetEntity asset, { 257 265 required bool showErrorMessage, 258 266 }) async { 267 + final errorUnableToAccessMedia = showErrorMessage 268 + ? AppLocalizations.of(context).errorUnableToAccessMedia 269 + : null; 270 + 259 271 try { 260 272 final file = await asset.file ?? await asset.originFile; 261 273 if (file == null) { 262 - if (showErrorMessage) { 263 - _showSnackBar('Unable to access this media item.'); 274 + if (errorUnableToAccessMedia != null) { 275 + _showSnackBar(errorUnableToAccessMedia); 264 276 } 265 277 return null; 266 278 } ··· 272 284 error: e, 273 285 stackTrace: stackTrace, 274 286 ); 275 - if (showErrorMessage) { 276 - _showSnackBar('Unable to access this media item.'); 287 + if (errorUnableToAccessMedia != null) { 288 + _showSnackBar(errorUnableToAccessMedia); 277 289 } 278 290 return null; 279 291 } ··· 308 320 Widget build(BuildContext context) { 309 321 final theme = Theme.of(context); 310 322 final colorScheme = theme.colorScheme; 323 + final l10n = AppLocalizations.of(context); 311 324 final multiSelectLabel = _isMultiPhotoSelection 312 - ? 'Single Select' 313 - : 'Select multiple'; 325 + ? l10n.labelSingleSelect 326 + : l10n.labelSelectMultiple; 314 327 315 328 return ClipRRect( 316 329 borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ··· 335 348 onPressed: () => Navigator.of(context).maybePop(), 336 349 icon: const Icon(Icons.close), 337 350 ), 338 - const Expanded( 351 + Expanded( 339 352 child: Text( 340 - 'Library', 353 + l10n.labelLibrary, 341 354 textAlign: TextAlign.center, 342 355 style: TextStyle( 343 356 fontSize: 18, ··· 372 385 ? null 373 386 : _submitMultiPhotoSelection, 374 387 child: Text( 375 - 'Done (${_selectedPhotoAssets.length}/${widget.maxMultiPhotoSelection})', 388 + l10n.labelDoneCount( 389 + _selectedPhotoAssets.length, 390 + widget.maxMultiPhotoSelection, 391 + ), 376 392 ), 377 393 ), 378 394 ], ··· 389 405 borderRadius: BorderRadius.circular(10), 390 406 ), 391 407 child: Text( 392 - 'Limited library access is enabled. ' 393 - 'You can change this in settings.', 408 + l10n.messageLimitedLibraryAccess, 394 409 style: theme.textTheme.bodySmall?.copyWith( 395 410 color: colorScheme.onSecondaryContainer, 396 411 ), ··· 418 433 children: [ 419 434 const Icon(Icons.photo_library_outlined, size: 40), 420 435 const SizedBox(height: 12), 421 - const Text( 422 - 'Allow photo library access to pick photos and videos.', 436 + Text( 437 + AppLocalizations.of(context).messagePermissionPhotoLibrary, 423 438 textAlign: TextAlign.center, 424 439 ), 425 440 const SizedBox(height: 16), 426 441 FilledButton( 427 442 onPressed: _requestPermissionAndLoadAssets, 428 - child: const Text('Allow Access'), 443 + child: Text(AppLocalizations.of(context).buttonAllowAccess), 429 444 ), 430 445 const SizedBox(height: 8), 431 - const TextButton( 446 + TextButton( 432 447 onPressed: PhotoManager.openSetting, 433 - child: Text('Open Settings'), 448 + child: Text(AppLocalizations.of(context).buttonOpenSettings), 434 449 ), 435 450 ], 436 451 ), ··· 448 463 } 449 464 450 465 if (_mediaAssets.isEmpty) { 451 - return const Center( 452 - child: Text('No photos or videos found in your library.'), 466 + return Center( 467 + child: Text(AppLocalizations.of(context).emptyNoPhotoLibrary), 453 468 ); 454 469 } 455 470
+24 -11
lib/src/features/posting/ui/pages/recording_page.dart
··· 6 6 import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 7 import 'package:get_it/get_it.dart'; 8 8 import 'package:pro_video_editor/pro_video_editor.dart'; 9 + import 'package:spark/src/core/l10n/app_localizations.dart'; 9 10 import 'package:spark/src/core/design_system/templates/recording_page_template.dart'; 10 11 import 'package:spark/src/core/pro_video_editor/models/video_editor_result.dart'; 11 12 import 'package:spark/src/core/pro_video_editor/pro_video_editor_repository.dart'; ··· 197 198 setState(() { 198 199 _isProcessing = false; 199 200 }); 200 - ScaffoldMessenger.of( 201 - context, 202 - ).showSnackBar(SnackBar(content: Text('Error: $e'))); 201 + ScaffoldMessenger.of(context).showSnackBar( 202 + SnackBar( 203 + content: Text( 204 + AppLocalizations.of(context).errorWithDetail(e.toString()), 205 + ), 206 + ), 207 + ); 203 208 } 204 209 } 205 210 } ··· 274 279 setState(() { 275 280 _isProcessing = false; 276 281 }); 277 - ScaffoldMessenger.of( 278 - context, 279 - ).showSnackBar(SnackBar(content: Text('Error: $e'))); 282 + ScaffoldMessenger.of(context).showSnackBar( 283 + SnackBar( 284 + content: Text( 285 + AppLocalizations.of(context).errorWithDetail(e.toString()), 286 + ), 287 + ), 288 + ); 280 289 } 281 290 } 282 291 } ··· 414 423 setState(() { 415 424 _isProcessing = false; 416 425 }); 417 - ScaffoldMessenger.of( 418 - context, 419 - ).showSnackBar(SnackBar(content: Text('Error: $e'))); 426 + ScaffoldMessenger.of(context).showSnackBar( 427 + SnackBar( 428 + content: Text( 429 + AppLocalizations.of(context).errorWithDetail(e.toString()), 430 + ), 431 + ), 432 + ); 420 433 } 421 434 } 422 435 } ··· 473 486 const SizedBox(height: 24), 474 487 ElevatedButton( 475 488 onPressed: () => context.router.pop(), 476 - child: const Text('Go Back'), 489 + child: Text(AppLocalizations.of(context).buttonGoBack), 477 490 ), 478 491 ], 479 492 ), ··· 611 624 const SizedBox(height: 24), 612 625 ElevatedButton( 613 626 onPressed: () => context.router.pop(), 614 - child: const Text('Go Back'), 627 + child: Text(AppLocalizations.of(context).buttonGoBack), 615 628 ), 616 629 ], 617 630 ),
+5 -4
lib/src/features/posting/ui/pages/story_post_page.dart
··· 8 8 import 'package:spark/src/core/network/atproto/data/models/models.dart'; 9 9 import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; 10 10 import 'package:spark/src/core/utils/error_messages.dart'; 11 + import 'package:spark/src/core/l10n/app_localizations.dart'; 11 12 import 'package:spark/src/features/posting/providers/post_story.dart'; 12 13 import 'package:spark/src/features/posting/providers/video_upload_provider.dart'; 13 14 ··· 95 96 } 96 97 97 98 setState(() { 98 - _statusMessage = 'Posting story...'; 99 + _statusMessage = AppLocalizations.of(context).messagePostingStory; 99 100 }); 100 101 101 102 final uploadedImage = uploadedImages.first; ··· 113 114 114 115 Future<void> _postVideoStory() async { 115 116 setState(() { 116 - _statusMessage = 'Processing video...'; 117 + _statusMessage = AppLocalizations.of(context).messageProcessingVideo; 117 118 }); 118 119 119 120 final result = await ref.read( ··· 301 302 borderRadius: BorderRadius.circular(12), 302 303 ), 303 304 ), 304 - child: const Text('Cancel'), 305 + child: Text(AppLocalizations.of(context).buttonCancel), 305 306 ), 306 307 ), 307 308 const SizedBox(width: 10), ··· 316 317 borderRadius: BorderRadius.circular(12), 317 318 ), 318 319 ), 319 - child: const Text('Retry'), 320 + child: Text(AppLocalizations.of(context).buttonRetry), 320 321 ), 321 322 ), 322 323 ],
+16 -14
lib/src/features/posting/ui/pages/video_review_page.dart
··· 8 8 import 'package:flutter_riverpod/flutter_riverpod.dart'; 9 9 import 'package:get_it/get_it.dart'; 10 10 import 'package:image_picker/image_picker.dart'; 11 + import 'package:spark/src/core/l10n/app_localizations.dart'; 11 12 import 'package:spark/src/core/design_system/templates/video_review_page_template.dart'; 12 13 import 'package:spark/src/core/design_system/tokens/constants.dart'; 13 14 import 'package:spark/src/core/network/atproto/atproto.dart'; ··· 220 221 }); 221 222 } 222 223 223 - String? get _uploadStatusLabel { 224 + String? _uploadStatusLabel(AppLocalizations l10n) { 224 225 if (_uploadErrorMessage != null) return _uploadErrorMessage; 225 226 return switch (_uploadPhase) { 226 - _VideoUploadPhase.uploading => 'Uploading video', 227 - _VideoUploadPhase.processing => 'Processing video', 228 - _VideoUploadPhase.ready => 'Ready to post', 227 + _VideoUploadPhase.uploading => l10n.messageUploadingVideo, 228 + _VideoUploadPhase.processing => l10n.messageProcessingVideo, 229 + _VideoUploadPhase.ready => l10n.messageReadyToPost, 229 230 null => null, 230 231 }; 231 232 } 232 233 233 - String get _postLabel { 234 - if (_uploadErrorMessage != null) return 'Upload failed'; 235 - if (_uploadResult != null) return 'Post'; 234 + String _postLabel(AppLocalizations l10n) { 235 + if (_uploadErrorMessage != null) return l10n.messageUploadFailed; 236 + if (_uploadResult != null) return l10n.buttonPost; 236 237 final percent = (_uploadProgress * 100).round(); 237 238 switch (_uploadPhase) { 238 239 case _VideoUploadPhase.uploading: 239 - return 'Uploading $percent%'; 240 + return l10n.messageUploadingPercent(percent); 240 241 case _VideoUploadPhase.processing: 241 - return 'Processing video'; 242 + return l10n.messageProcessingVideo; 242 243 case _VideoUploadPhase.ready: 243 - return 'Post'; 244 + return l10n.buttonPost; 244 245 case null: 245 - return 'Uploading video'; 246 + return l10n.messageUploadingVideo; 246 247 } 247 248 } 248 249 ··· 326 327 327 328 @override 328 329 Widget build(BuildContext context) { 330 + final l10n = AppLocalizations.of(context); 329 331 final rawAspectRatio = _player?.value.aspectRatio; 330 332 final ar = rawAspectRatio != null && rawAspectRatio > 0 331 333 ? rawAspectRatio 332 334 : 1.0; 333 335 final textLength = _descriptionController.text.runes.length; 334 336 final isOverLimit = textLength > AppConstants.postDescriptionMaxChars; 335 - final uploadStatusLabel = _uploadStatusLabel; 337 + final uploadStatusLabel = _uploadStatusLabel(l10n); 336 338 final canPost = 337 339 !_isPosting && 338 340 _uploadResult != null && ··· 340 342 !isOverLimit; 341 343 342 344 return VideoReviewPageTemplate( 343 - title: 'Review Video', 345 + title: l10n.pageTitleReviewVideo, 344 346 onBack: () => context.maybePop(), 345 347 aspectRatio: ar, 346 348 videoPreview: _player == null ··· 362 364 showCrossPost: !widget.storyMode, 363 365 crossPostValue: _crosspostToBsky, 364 366 onCrossPostChanged: (v) => setState(() => _crosspostToBsky = v), 365 - postLabel: _postLabel, 367 + postLabel: _postLabel(l10n), 366 368 isPosting: _isPosting, 367 369 isOverLimit: isOverLimit, 368 370 onPost: canPost
+1 -1
lib/src/features/posting/ui/widgets/mention_input_field.dart
··· 11 11 const MentionInputField({ 12 12 required this.controller, 13 13 required this.onMentionsChanged, 14 - this.hintText = 'Add a description... (optional)', 14 + required this.hintText, 15 15 this.maxChars = AppConstants.postDescriptionMaxChars, 16 16 this.maxLines = 5, 17 17 this.minLines = 1,
+7 -2
lib/src/features/posting/utils/story_direct_post.dart
··· 4 4 import 'package:get_it/get_it.dart'; 5 5 import 'package:image_picker/image_picker.dart'; 6 6 import 'package:spark/src/core/design_system/tokens/colors.dart'; 7 + import 'package:spark/src/core/l10n/app_localizations.dart'; 7 8 import 'package:spark/src/core/network/atproto/data/models/models.dart'; 8 9 import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; 9 10 import 'package:spark/src/features/posting/providers/post_story.dart'; ··· 29 30 showDialog( 30 31 context: context, 31 32 barrierDismissible: false, 32 - builder: (_) => const _PostingOverlay(message: 'Posting story...'), 33 + builder: (_) => _PostingOverlay( 34 + message: AppLocalizations.of(context).messagePostingStory, 35 + ), 33 36 ); 34 37 35 38 try { ··· 91 94 showDialog( 92 95 context: context, 93 96 barrierDismissible: false, 94 - builder: (_) => const _PostingOverlay(message: 'Processing video...'), 97 + builder: (_) => _PostingOverlay( 98 + message: AppLocalizations.of(context).messageProcessingVideo, 99 + ), 95 100 ); 96 101 97 102 try {
+37 -22
lib/src/features/profile/ui/pages/profile_page.dart
··· 19 19 import 'package:spark/src/core/utils/blocking_utils.dart'; 20 20 import 'package:spark/src/core/utils/logging/log_service.dart'; 21 21 import 'package:spark/src/core/utils/logging/logger.dart'; 22 + import 'package:spark/src/core/l10n/app_localizations.dart'; 22 23 import 'package:spark/src/core/utils/text_formatter.dart'; 23 24 import 'package:spark/src/features/auth/providers/auth_providers.dart'; 24 25 import 'package:spark/src/features/posting/ui/pages/recording_page.dart'; ··· 237 238 currentUserDid != null && currentUserDid == widget.did; 238 239 final colorScheme = theme.colorScheme; 239 240 241 + final l10n = AppLocalizations.of(context); 240 242 return ErrorScreen( 241 243 context: context, 242 - message: 'Profile not found', 244 + message: l10n.errorProfileNotFound, 243 245 stackTrace: null, 244 246 onRetry: notifier.refreshProfile, 245 247 theme: theme, ··· 378 380 // Show confirmation dialog 379 381 final confirmed = await showDialog<bool>( 380 382 context: context, 381 - builder: (context) => AlertDialog( 382 - title: Text(wasBlocked ? 'Unblock User' : 'Block User'), 383 - content: Text( 384 - wasBlocked 385 - ? 'Are you sure you want to unblock this user?' 386 - : 'Are you sure you want to block this user? ' 387 - 'You will no longer see their posts.', 388 - ), 389 - actions: [ 390 - TextButton( 391 - onPressed: () => Navigator.of(context).pop(false), 392 - child: const Text('Cancel'), 383 + builder: (context) { 384 + final dialogL10n = AppLocalizations.of(context); 385 + return AlertDialog( 386 + title: Text( 387 + wasBlocked 388 + ? dialogL10n.dialogUnblockUser 389 + : dialogL10n.dialogBlockUser, 393 390 ), 394 - TextButton( 395 - onPressed: () => Navigator.of(context).pop(true), 396 - style: TextButton.styleFrom( 397 - foregroundColor: wasBlocked ? null : Colors.red, 391 + content: Text( 392 + wasBlocked 393 + ? dialogL10n.dialogUnblockUserConfirm 394 + : dialogL10n.dialogBlockUserConfirm, 395 + ), 396 + actions: [ 397 + TextButton( 398 + onPressed: () => Navigator.of(context).pop(false), 399 + child: Text(dialogL10n.buttonCancel), 398 400 ), 399 - child: Text(wasBlocked ? 'Unblock' : 'Block'), 400 - ), 401 - ], 402 - ), 401 + TextButton( 402 + onPressed: () => Navigator.of(context).pop(true), 403 + style: TextButton.styleFrom( 404 + foregroundColor: wasBlocked ? null : Colors.red, 405 + ), 406 + child: Text( 407 + wasBlocked 408 + ? dialogL10n.buttonUnblock 409 + : dialogL10n.buttonBlock, 410 + ), 411 + ), 412 + ], 413 + ); 414 + }, 403 415 ); 404 416 405 417 if (confirmed != true) return; ··· 672 684 textAlign: TextAlign.center, 673 685 ), 674 686 const SizedBox(height: 16), 675 - TextButton(onPressed: onRetry, child: const Text('Retry')), 687 + TextButton( 688 + onPressed: onRetry, 689 + child: Text(AppLocalizations.of(context).buttonRetry), 690 + ), 676 691 ], 677 692 ), 678 693 ),
+5 -2
lib/src/features/profile/ui/pages/standalone_likes_feed_page.dart
··· 5 5 import 'package:flutter/material.dart'; 6 6 import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 7 import 'package:spark/src/core/design_system/components/atoms/buttons/app_overlay_back_button.dart'; 8 + import 'package:spark/src/core/l10n/app_localizations.dart'; 8 9 import 'package:spark/src/core/design_system/tokens/constants.dart'; 9 10 import 'package:spark/src/core/routing/app_router.dart'; 10 11 import 'package:spark/src/core/ui/foundation/colors.dart'; ··· 148 149 ), 149 150 const SizedBox(height: 16), 150 151 Text( 151 - 'Error loading likes: $error', 152 + AppLocalizations.of( 153 + context, 154 + ).errorWithDetail(error.toString()), 152 155 style: const TextStyle(color: AppColors.white), 153 156 textAlign: TextAlign.center, 154 157 ), ··· 164 167 ) 165 168 .refresh(); 166 169 }, 167 - child: const Text('Retry'), 170 + child: Text(AppLocalizations.of(context).buttonRetry), 168 171 ), 169 172 ], 170 173 ),
+5 -2
lib/src/features/profile/ui/pages/standalone_profile_feed_page.dart
··· 5 5 import 'package:flutter/material.dart'; 6 6 import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 7 import 'package:spark/src/core/design_system/components/atoms/buttons/app_overlay_back_button.dart'; 8 + import 'package:spark/src/core/l10n/app_localizations.dart'; 8 9 import 'package:spark/src/core/design_system/tokens/constants.dart'; 9 10 import 'package:spark/src/core/routing/app_router.dart'; 10 11 import 'package:spark/src/core/ui/foundation/colors.dart'; ··· 153 154 ), 154 155 const SizedBox(height: 16), 155 156 Text( 156 - 'Error loading feed: $error', 157 + AppLocalizations.of( 158 + context, 159 + ).errorWithDetail(error.toString()), 157 160 style: const TextStyle(color: AppColors.white), 158 161 textAlign: TextAlign.center, 159 162 ), ··· 170 173 ) 171 174 .refresh(); 172 175 }, 173 - child: const Text('Retry'), 176 + child: Text(AppLocalizations.of(context).buttonRetry), 174 177 ), 175 178 ], 176 179 ),
+5 -2
lib/src/features/profile/ui/pages/standalone_reposts_feed_page.dart
··· 5 5 import 'package:flutter/material.dart'; 6 6 import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 7 import 'package:spark/src/core/design_system/components/atoms/buttons/app_overlay_back_button.dart'; 8 + import 'package:spark/src/core/l10n/app_localizations.dart'; 8 9 import 'package:spark/src/core/design_system/tokens/constants.dart'; 9 10 import 'package:spark/src/core/routing/app_router.dart'; 10 11 import 'package:spark/src/core/ui/foundation/colors.dart'; ··· 150 151 ), 151 152 const SizedBox(height: 16), 152 153 Text( 153 - 'Error loading reposts: $error', 154 + AppLocalizations.of( 155 + context, 156 + ).errorWithDetail(error.toString()), 154 157 style: const TextStyle(color: AppColors.white), 155 158 textAlign: TextAlign.center, 156 159 ), ··· 166 169 ) 167 170 .refresh(); 168 171 }, 169 - child: const Text('Retry'), 172 + child: Text(AppLocalizations.of(context).buttonRetry), 170 173 ), 171 174 ], 172 175 ),
+10 -4
lib/src/features/profile/ui/pages/user_list_page.dart
··· 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 4 import 'package:spark/src/core/design_system/components/atoms/buttons/app_leading_button.dart'; 5 + import 'package:spark/src/core/l10n/app_localizations.dart'; 5 6 import 'package:spark/src/features/profile/providers/user_list_provider.dart'; 6 7 import 'package:spark/src/features/profile/ui/widgets/user_list_view.dart'; 7 8 ··· 49 50 final userListAsync = ref.watch( 50 51 userListProvider(did: widget.did, type: widget.type), 51 52 ); 53 + final l10n = AppLocalizations.of(context); 52 54 final title = widget.type == UserListType.followers 53 - ? 'Followers' 54 - : 'Following'; 55 + ? l10n.pageTitleFollowers 56 + : l10n.pageTitleFollowing; 55 57 56 58 return Scaffold( 57 59 appBar: AppBar( 58 - leading: const AppLeadingButton(tooltip: 'Back'), 60 + leading: AppLeadingButton(tooltip: l10n.tooltipBack), 59 61 title: Text(title), 60 62 ), 61 63 body: RefreshIndicator( ··· 80 82 Center( 81 83 child: Padding( 82 84 padding: const EdgeInsets.all(16), 83 - child: Text('An error occurred: $error'), 85 + child: Text( 86 + AppLocalizations.of( 87 + context, 88 + ).errorWithDetail(error.toString()), 89 + ), 84 90 ), 85 91 ), 86 92 ],
+3 -3
lib/src/features/profile/ui/pages/user_profile_page.dart
··· 1 1 import 'package:auto_route/auto_route.dart'; 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 + import 'package:spark/src/core/l10n/app_localizations.dart'; 4 5 import 'package:spark/src/features/auth/providers/auth_providers.dart'; 5 6 import 'package:spark/src/features/profile/ui/pages/profile_page.dart'; 6 7 ··· 13 14 final currentUserDid = ref.watch(currentDidProvider); 14 15 15 16 if (currentUserDid == null) { 16 - return const Scaffold( 17 - body: Center(child: Text('Please log in to view your profile')), 18 - ); 17 + final l10n = AppLocalizations.of(context); 18 + return Scaffold(body: Center(child: Text(l10n.messagePleaseLogin))); 19 19 } 20 20 21 21 // Use the existing ProfilePage but pass the current user's DID
+5 -2
lib/src/features/profile/ui/widgets/profile_grid_widget.dart
··· 4 4 import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 5 import 'package:skeletonizer/skeletonizer.dart'; 6 6 import 'package:spark/src/core/design_system/components/molecules/post_tile.dart'; 7 + import 'package:spark/src/core/l10n/app_localizations.dart'; 7 8 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 8 9 import 'package:spark/src/features/profile/providers/profile_feed_provider.dart'; 9 10 ··· 123 124 children: [ 124 125 const Icon(FluentIcons.error_circle_24_regular, size: 48), 125 126 const SizedBox(height: 16), 126 - Text('Error loading posts: $error'), 127 + Text( 128 + AppLocalizations.of(context).errorWithDetail(error.toString()), 129 + ), 127 130 const SizedBox(height: 16), 128 131 ElevatedButton( 129 132 onPressed: () => ref ··· 135 138 ).notifier, 136 139 ) 137 140 .refresh(), 138 - child: const Text('Retry'), 141 + child: Text(AppLocalizations.of(context).buttonRetry), 139 142 ), 140 143 ], 141 144 ),
+7 -2
lib/src/features/profile/ui/widgets/profile_likes_tab.dart
··· 4 4 import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 5 import 'package:skeletonizer/skeletonizer.dart'; 6 6 import 'package:spark/src/core/design_system/components/atoms/icons.dart'; 7 + import 'package:spark/src/core/l10n/app_localizations.dart'; 7 8 import 'package:spark/src/core/routing/app_router.dart'; 8 9 import 'package:spark/src/features/profile/providers/profile_likes_provider.dart'; 9 10 import 'package:spark/src/features/profile/ui/widgets/profile_grid_widget.dart'; ··· 166 167 children: [ 167 168 const Icon(Icons.error_outline, size: 48), 168 169 const SizedBox(height: 16), 169 - Text('Error loading likes: $error'), 170 + Text( 171 + AppLocalizations.of( 172 + context, 173 + ).errorWithDetail(error.toString()), 174 + ), 170 175 const SizedBox(height: 16), 171 176 ElevatedButton( 172 177 onPressed: () => ref 173 178 .read(profileLikesProvider(actor, bsky).notifier) 174 179 .refresh(), 175 - child: const Text('Retry'), 180 + child: Text(AppLocalizations.of(context).buttonRetry), 176 181 ), 177 182 ], 178 183 ),
+7 -2
lib/src/features/profile/ui/widgets/profile_reposts_tab.dart
··· 4 4 import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 5 import 'package:skeletonizer/skeletonizer.dart'; 6 6 import 'package:spark/src/core/design_system/components/atoms/icons.dart'; 7 + import 'package:spark/src/core/l10n/app_localizations.dart'; 7 8 import 'package:spark/src/core/routing/app_router.dart'; 8 9 import 'package:spark/src/features/profile/providers/profile_reposts_provider.dart'; 9 10 import 'package:spark/src/features/profile/ui/widgets/profile_grid_widget.dart'; ··· 166 167 children: [ 167 168 const Icon(Icons.error_outline, size: 48), 168 169 const SizedBox(height: 16), 169 - Text('Error loading reposts: $error'), 170 + Text( 171 + AppLocalizations.of( 172 + context, 173 + ).errorWithDetail(error.toString()), 174 + ), 170 175 const SizedBox(height: 16), 171 176 ElevatedButton( 172 177 onPressed: () => ref 173 178 .read(profileRepostsProvider(actor, bsky).notifier) 174 179 .refresh(), 175 - child: const Text('Retry'), 180 + child: Text(AppLocalizations.of(context).buttonRetry), 176 181 ), 177 182 ], 178 183 ),
+3 -3
lib/src/features/search/ui/pages/search_page.dart
··· 129 129 : null, 130 130 ), 131 131 showTabs: showSubmittedResults, 132 - tabsWidget: const TabBar( 132 + tabsWidget: TabBar( 133 133 tabs: [ 134 - Tab(text: 'Posts'), 135 - Tab(text: 'Users'), 134 + Tab(text: l10n.tabPosts), 135 + Tab(text: l10n.tabUsers), 136 136 ], 137 137 ), 138 138 contentWidget: const TabBarView(
+1 -1
lib/src/features/settings/ui/pages/feed_list_page.dart
··· 52 52 }, 53 53 icon: Icon(_isEditMode ? Icons.check : Icons.edit, size: 18), 54 54 label: Text( 55 - _isEditMode ? 'Done' : 'Edit', 55 + _isEditMode ? l10n.buttonDone : l10n.buttonEdit, 56 56 style: const TextStyle(fontSize: 14), 57 57 ), 58 58 style: TextButton.styleFrom(
+9 -8
lib/src/features/settings/ui/pages/labeler_label_settings_page.dart
··· 11 11 import 'package:spark/src/core/network/atproto/data/models/labeler_models.dart'; 12 12 import 'package:spark/src/core/network/atproto/data/repositories/actor_repository.dart'; 13 13 import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; 14 + import 'package:spark/src/core/l10n/app_localizations.dart'; 14 15 import 'package:spark/src/core/utils/logging/logging.dart'; 15 16 import 'package:spark/src/features/settings/providers/settings_provider.dart'; 16 17 import 'package:spark/src/features/settings/ui/widgets/widgets.dart'; ··· 289 290 Widget build(BuildContext context) { 290 291 final theme = Theme.of(context); 291 292 final colorScheme = theme.colorScheme; 293 + final l10n = AppLocalizations.of(context); 292 294 293 295 if (_isLoading) { 294 296 return Scaffold( ··· 302 304 titleTextStyle: theme.appBarTheme.titleTextStyle?.copyWith( 303 305 color: colorScheme.onSurface, 304 306 ), 305 - title: const Text('Labeler Settings'), 307 + title: Text(l10n.pageTitleLabelerSettings), 306 308 centerTitle: true, 307 309 leading: const AppLeadingButton(), 308 310 ), ··· 322 324 titleTextStyle: theme.appBarTheme.titleTextStyle?.copyWith( 323 325 color: colorScheme.onSurface, 324 326 ), 325 - title: const Text('Labeler Settings'), 327 + title: Text(l10n.pageTitleLabelerSettings), 326 328 centerTitle: true, 327 329 leading: const AppLeadingButton(), 328 330 ), ··· 335 337 Icon(Icons.error_outline, size: 48, color: colorScheme.error), 336 338 const SizedBox(height: 16), 337 339 Text( 338 - 'Error Loading Labeler Settings', 340 + l10n.errorLoadingLabelerSettings, 339 341 style: TextStyle( 340 342 fontSize: 18, 341 343 fontWeight: FontWeight.bold, ··· 351 353 const SizedBox(height: 16), 352 354 ElevatedButton( 353 355 onPressed: _loadLabelerSettings, 354 - child: const Text('Retry'), 356 + child: Text(l10n.buttonRetry), 355 357 ), 356 358 ], 357 359 ), ··· 371 373 titleTextStyle: theme.appBarTheme.titleTextStyle?.copyWith( 372 374 color: colorScheme.onSurface, 373 375 ), 374 - title: const Text('Labeler Settings'), 376 + title: Text(l10n.pageTitleLabelerSettings), 375 377 centerTitle: true, 376 378 leading: const AppLeadingButton(), 377 379 ), ··· 457 459 crossAxisAlignment: CrossAxisAlignment.start, 458 460 children: [ 459 461 Text( 460 - 'Content Label Settings', 462 + l10n.labelContentLabelSettings, 461 463 style: TextStyle( 462 464 fontSize: 20, 463 465 fontWeight: FontWeight.bold, ··· 466 468 ), 467 469 const SizedBox(height: 8), 468 470 Text( 469 - "Configure how this labeler's content labels " 470 - 'are handled in your feeds.', 471 + l10n.messageLabelerConfigDescription, 471 472 style: TextStyle( 472 473 color: colorScheme.onSurface.withAlpha(178), 473 474 fontSize: 14,
+29 -17
lib/src/features/settings/ui/pages/legal_page.dart
··· 3 3 import 'package:flutter/material.dart'; 4 4 import 'package:get_it/get_it.dart'; 5 5 import 'package:spark/src/core/design_system/components/atoms/buttons/app_leading_button.dart'; 6 + import 'package:spark/src/core/l10n/app_localizations.dart'; 6 7 import 'package:spark/src/core/utils/logging/log_service.dart'; 7 8 import 'package:url_launcher/url_launcher.dart'; 8 9 ··· 10 11 class LegalPage extends StatelessWidget { 11 12 const LegalPage({super.key}); 12 13 13 - static const List<({String title, String path})> _legalLinks = [ 14 - (title: 'Privacy Policy', path: '/privacy'), 15 - (title: 'Terms of Service', path: '/terms'), 16 - (title: 'Support', path: '/support'), 17 - ]; 14 + static const List<String> _legalPaths = ['/privacy', '/terms', '/support']; 18 15 19 16 static final Uri _baseUri = Uri.parse('https://sprk.so'); 20 17 18 + String _titleForPath(String path, AppLocalizations l10n) { 19 + switch (path) { 20 + case '/privacy': 21 + return l10n.labelPrivacyPolicy; 22 + case '/terms': 23 + return l10n.labelTermsOfService; 24 + case '/support': 25 + return l10n.labelSupport; 26 + default: 27 + return path; 28 + } 29 + } 30 + 21 31 Future<void> _openLink(BuildContext context, String path) async { 32 + final l10n = AppLocalizations.of(context); 22 33 final logger = GetIt.instance<LogService>().getLogger('LegalPage'); 23 34 final uri = _baseUri.replace(path: path); 24 35 ··· 29 40 ); 30 41 31 42 if (!didLaunch && context.mounted) { 32 - ScaffoldMessenger.of(context).showSnackBar( 33 - const SnackBar(content: Text('Unable to open link right now.')), 34 - ); 43 + ScaffoldMessenger.of( 44 + context, 45 + ).showSnackBar(SnackBar(content: Text(l10n.errorUnableToOpenLink))); 35 46 } 36 47 } catch (error, stackTrace) { 37 48 logger.e( ··· 41 52 ); 42 53 43 54 if (context.mounted) { 44 - ScaffoldMessenger.of(context).showSnackBar( 45 - const SnackBar(content: Text('Unable to open link right now.')), 46 - ); 55 + ScaffoldMessenger.of( 56 + context, 57 + ).showSnackBar(SnackBar(content: Text(l10n.errorUnableToOpenLink))); 47 58 } 48 59 } 49 60 } ··· 51 62 @override 52 63 Widget build(BuildContext context) { 53 64 final colorScheme = Theme.of(context).colorScheme; 65 + final l10n = AppLocalizations.of(context); 54 66 55 67 return Scaffold( 56 68 backgroundColor: colorScheme.surface, ··· 59 71 elevation: 0, 60 72 scrolledUnderElevation: 0, 61 73 leading: const AppLeadingButton(), 62 - title: const Text('Legal'), 74 + title: Text(l10n.pageTitleLegal), 63 75 centerTitle: true, 64 76 ), 65 77 body: ListView.separated( 66 78 padding: const EdgeInsets.all(16), 67 - itemCount: _legalLinks.length, 79 + itemCount: _legalPaths.length, 68 80 separatorBuilder: (_, _) => const SizedBox(height: 16), 69 81 itemBuilder: (context, index) { 70 - final link = _legalLinks[index]; 82 + final path = _legalPaths[index]; 71 83 72 84 return Container( 73 85 decoration: BoxDecoration( ··· 76 88 ), 77 89 child: ListTile( 78 90 title: Text( 79 - link.title, 91 + _titleForPath(path, l10n), 80 92 style: const TextStyle( 81 93 fontSize: 16, 82 94 fontWeight: FontWeight.bold, 83 95 ), 84 96 ), 85 - subtitle: Text('sprk.so${link.path}'), 97 + subtitle: Text('sprk.so$path'), 86 98 trailing: const Icon(FluentIcons.open_24_regular), 87 99 contentPadding: const EdgeInsets.symmetric( 88 100 horizontal: 16, 89 101 vertical: 4, 90 102 ), 91 - onTap: () => _openLink(context, link.path), 103 + onTap: () => _openLink(context, path), 92 104 ), 93 105 ); 94 106 },
+11 -14
lib/src/features/settings/ui/pages/settings_page.dart
··· 52 52 } 53 53 54 54 Future<void> _handleManageAccount() async { 55 + final l10n = AppLocalizations.of(context); 55 56 final authRepository = GetIt.instance<AuthRepository>(); 56 57 final pdsUrl = authRepository.pdsEndpoint ?? 'your PDS URL'; 57 58 final shouldOpen = await showDialog<bool>( 58 59 context: context, 59 60 builder: (context) => AlertDialog( 60 - title: const Text('Open Bluesky account management?'), 61 - content: Text( 62 - 'This opens the Bluesky account management screen. ' 63 - 'You may have to log in again.\n\n' 64 - 'If prompted for an account provider, use:\n$pdsUrl', 65 - ), 61 + title: Text(l10n.dialogOpenBlueskyAccount), 62 + content: Text(l10n.dialogOpenBlueskyAccountDescription(pdsUrl)), 66 63 actions: [ 67 64 TextButton( 68 65 onPressed: () => Navigator.of(context).pop(false), 69 - child: const Text('Cancel'), 66 + child: Text(l10n.buttonCancel), 70 67 ), 71 68 FilledButton( 72 69 onPressed: () => Navigator.of(context).pop(true), 73 - child: const Text('Open'), 70 + child: Text(l10n.buttonOpen), 74 71 ), 75 72 ], 76 73 ), ··· 89 86 ); 90 87 91 88 if (!didLaunch && mounted) { 92 - ScaffoldMessenger.of(context).showSnackBar( 93 - const SnackBar(content: Text('Unable to open link right now.')), 94 - ); 89 + ScaffoldMessenger.of( 90 + context, 91 + ).showSnackBar(SnackBar(content: Text(l10n.errorUnableToOpenLink))); 95 92 } 96 93 } catch (error, stackTrace) { 97 94 logger.e( ··· 101 98 ); 102 99 103 100 if (mounted) { 104 - ScaffoldMessenger.of(context).showSnackBar( 105 - const SnackBar(content: Text('Unable to open link right now.')), 106 - ); 101 + ScaffoldMessenger.of( 102 + context, 103 + ).showSnackBar(SnackBar(content: Text(l10n.errorUnableToOpenLink))); 107 104 } 108 105 } 109 106 }
+6 -4
lib/src/features/sound/ui/pages/sound_page.dart
··· 5 5 import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 6 import 'package:spark/src/core/design_system/components/atoms/buttons/app_leading_button.dart'; 7 7 import 'package:spark/src/core/design_system/components/molecules/post_tile.dart'; 8 + import 'package:spark/src/core/l10n/app_localizations.dart'; 8 9 import 'package:spark/src/core/network/atproto/data/models/models.dart'; 9 10 import 'package:spark/src/core/routing/app_router.dart'; 10 11 import 'package:spark/src/features/sound/providers/sound_page_provider.dart'; ··· 51 52 final soundState = ref.watch(soundPageProvider(_audioAtUri)); 52 53 final theme = Theme.of(context); 53 54 final colorScheme = theme.colorScheme; 55 + final l10n = AppLocalizations.of(context); 54 56 55 57 return Scaffold( 56 58 appBar: AppBar( 57 59 centerTitle: true, 58 - title: const Text('Sound'), 60 + title: Text(l10n.pageTitleSound), 59 61 elevation: 0, 60 62 leading: const AppLeadingButton(), 61 63 ), ··· 93 95 ), 94 96 const SizedBox(height: 16), 95 97 Text( 96 - 'No videos using this sound yet', 98 + l10n.emptyNoVideosUsingSound, 97 99 style: theme.textTheme.bodyLarge?.copyWith( 98 100 color: colorScheme.onSurfaceVariant, 99 101 ), ··· 153 155 children: [ 154 156 const Icon(FluentIcons.error_circle_24_regular, size: 48), 155 157 const SizedBox(height: 16), 156 - Text('Error loading sound: $error'), 158 + Text(l10n.errorWithDetail(error.toString())), 157 159 const SizedBox(height: 16), 158 160 ElevatedButton( 159 161 onPressed: () => 160 162 ref.read(soundPageProvider(_audioAtUri).notifier).refresh(), 161 - child: const Text('Retry'), 163 + child: Text(l10n.buttonRetry), 162 164 ), 163 165 ], 164 166 ),
+3 -1
lib/src/features/stories/ui/pages/all_stories_page.dart
··· 1 1 import 'package:auto_route/auto_route.dart'; 2 2 import 'package:flutter/material.dart'; 3 + import 'package:spark/src/core/l10n/app_localizations.dart'; 3 4 import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; 4 5 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 5 6 import 'package:spark/src/features/stories/ui/pages/author_stories_page.dart'; ··· 40 41 @override 41 42 Widget build(BuildContext context) { 42 43 if (_authorsList.isEmpty) { 43 - return const Scaffold(body: Center(child: Text('No stories'))); 44 + final l10n = AppLocalizations.of(context); 45 + return Scaffold(body: Center(child: Text(l10n.emptyNoStories))); 44 46 } 45 47 46 48 return Scaffold(
+3 -1
lib/src/features/stories/ui/pages/author_stories_page.dart
··· 4 4 import 'package:cached_network_image/cached_network_image.dart'; 5 5 import 'package:flutter/material.dart'; 6 6 import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 + import 'package:spark/src/core/l10n/app_localizations.dart'; 7 8 import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; 8 9 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 9 10 import 'package:spark/src/core/routing/app_router.dart'; ··· 248 249 @override 249 250 Widget build(BuildContext context) { 250 251 if (widget.stories.isEmpty) { 251 - return const Scaffold(body: Center(child: Text('No stories'))); 252 + final l10n = AppLocalizations.of(context); 253 + return Scaffold(body: Center(child: Text(l10n.emptyNoStories))); 252 254 } 253 255 254 256 return Scaffold(
+2 -1
lib/src/features/stories/ui/widgets/stories_list.dart
··· 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 4 import 'package:spark/src/core/design_system/components/molecules/story_circle.dart'; 5 + import 'package:spark/src/core/l10n/app_localizations.dart'; 5 6 import 'package:spark/src/core/routing/app_router.dart'; 6 7 import 'package:spark/src/features/auth/providers/auth_providers.dart'; 7 8 import 'package:spark/src/features/posting/ui/pages/recording_page.dart'; ··· 46 47 const Spacer(), 47 48 IconButton( 48 49 icon: const Icon(Icons.manage_history_outlined, size: 20), 49 - tooltip: 'Manage', 50 + tooltip: AppLocalizations.of(context).tooltipManage, 50 51 onPressed: () => context.router.push(const StoryManagerRoute()), 51 52 ), 52 53 ],