mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
3
fork

Configure Feed

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

feat: i10n expansion (#30)

* feat: migrate feeds and posts to i10n

* feat: migrate composition flow to i10n

* feat: migrate profiles to i10n

* feat: migrate 'alerts' (messages, notifications, account switcher) to i10n

* feat: migrate moderation, settings, and dev focused stuff to i10n

authored by

Owais and committed by
GitHub
42cf8080 e081de24

+8811 -984
+6 -11
docs/tasks/internationalization.md
··· 19 19 20 20 ## M2 - Remaining Feature Surfaces 21 21 22 - - [ ] Localize feed cards, post menus, post actions, saved posts, and trending 23 - - [ ] Localize compose flow, media alt text editors, draft/schedule states, and validation 24 - - [ ] Localize profile screens, profile actions, reports, follows, lists, and starter packs 25 - - [ ] Localize messages, notifications, alerts, and account switching sheets 26 - - [ ] Localize moderation settings/detail screens and logs/devtools user-facing labels 22 + - [x] Localize feed cards, post menus, post actions, saved posts, and trending 23 + - [x] Localize compose flow, media alt text editors, draft/schedule states, and validation 24 + - [x] Localize profile screens, profile actions, reports, follows, lists, and starter packs 25 + - [x] Localize messages, notifications, alerts, and account switching sheets 26 + - [x] Localize moderation settings/detail screens and logs/devtools user-facing labels 27 27 28 - ## M3 - Locale QA and Secondary-Locale Strategy 28 + ## M3 - User-Facing Language Selection 29 29 30 30 - [ ] Add a pseudo-locale or generated QA locale for layout stress testing 31 - - [ ] Audit hard-coded visible strings after M2 32 - - [ ] Add locale-aware golden/widget coverage for dense layouts 33 31 - [ ] Decide first real non-English locale and translation ownership 34 - 35 - ## M4 - User-Facing Language Selection 36 - 37 32 - [ ] Add persisted language preference only after multiple real locales exist 38 33 - [ ] Add settings UI for system/default language and supported overrides 39 34 - [ ] Add tests for locale override persistence and app rebuild behavior
+3144
lib/core/l10n/app_localizations.dart
··· 109 109 /// **'Cancel'** 110 110 String get buttonCancel; 111 111 112 + /// Tooltip for composing a post 113 + /// 114 + /// In en, this message translates to: 115 + /// **'Compose'** 116 + String get buttonCompose; 117 + 112 118 /// Button label to clear local cache 113 119 /// 114 120 /// In en, this message translates to: ··· 121 127 /// **'Continue'** 122 128 String get buttonContinue; 123 129 130 + /// Button label to clear locally stored items 131 + /// 132 + /// In en, this message translates to: 133 + /// **'Clear Local'** 134 + String get buttonClearLocal; 135 + 136 + /// Delete button label 137 + /// 138 + /// In en, this message translates to: 139 + /// **'Delete'** 140 + String get buttonDelete; 141 + 142 + /// Discard button label 143 + /// 144 + /// In en, this message translates to: 145 + /// **'Discard'** 146 + String get buttonDiscard; 147 + 148 + /// Button label to load additional quote posts 149 + /// 150 + /// In en, this message translates to: 151 + /// **'Load more quotes'** 152 + String get buttonLoadMoreQuotes; 153 + 154 + /// Button label to load additional repost users 155 + /// 156 + /// In en, this message translates to: 157 + /// **'Load more reposts'** 158 + String get buttonLoadMoreReposts; 159 + 124 160 /// Remove button label 125 161 /// 126 162 /// In en, this message translates to: ··· 151 187 /// **'Open'** 152 188 String get buttonOpen; 153 189 190 + /// OK button label 191 + /// 192 + /// In en, this message translates to: 193 + /// **'OK'** 194 + String get buttonOk; 195 + 196 + /// Button label to publish a post 197 + /// 198 + /// In en, this message translates to: 199 + /// **'Post'** 200 + String get buttonPost; 201 + 154 202 /// Button label to clear local sign-in data 155 203 /// 156 204 /// In en, this message translates to: ··· 163 211 /// **'Retry'** 164 212 String get buttonRetry; 165 213 214 + /// Save button label 215 + /// 216 + /// In en, this message translates to: 217 + /// **'Save'** 218 + String get buttonSave; 219 + 220 + /// Button label to save post edits 221 + /// 222 + /// In en, this message translates to: 223 + /// **'Save Changes'** 224 + String get buttonSaveChanges; 225 + 166 226 /// Sign in button label 167 227 /// 168 228 /// In en, this message translates to: ··· 175 235 /// **'Show content'** 176 236 String get buttonShowContent; 177 237 238 + /// Share button or menu label 239 + /// 240 + /// In en, this message translates to: 241 + /// **'Share'** 242 + String get buttonShare; 243 + 178 244 /// Try again button label 179 245 /// 180 246 /// In en, this message translates to: ··· 193 259 /// **'None'** 194 260 String get commonNone; 195 261 262 + /// Lowercase relative time label for the current moment 263 + /// 264 + /// In en, this message translates to: 265 + /// **'now'** 266 + String get commonNow; 267 + 268 + /// Relative time label for the current moment 269 + /// 270 + /// In en, this message translates to: 271 + /// **'Just now'** 272 + String get commonJustNow; 273 + 196 274 /// Label for a health check that has not run 197 275 /// 198 276 /// In en, this message translates to: ··· 205 283 /// **'Off'** 206 284 String get commonOff; 207 285 286 + /// Lowercase fallback for an unknown value 287 + /// 288 + /// In en, this message translates to: 289 + /// **'unknown'** 290 + String get commonUnknown; 291 + 208 292 /// Confirmation dialog body before clearing local cache 209 293 /// 210 294 /// In en, this message translates to: ··· 217 301 /// **'Clear cache?'** 218 302 String get dialogClearCacheTitle; 219 303 304 + /// Confirmation dialog body before clearing local bookmarks 305 + /// 306 + /// In en, this message translates to: 307 + /// **'This removes only local bookmarks from this device. Bluesky cloud bookmarks will not be deleted.'** 308 + String get dialogClearLocalBookmarksContent; 309 + 310 + /// Confirmation dialog title before clearing local bookmarks 311 + /// 312 + /// In en, this message translates to: 313 + /// **'Clear local bookmarks?'** 314 + String get dialogClearLocalBookmarksTitle; 315 + 316 + /// Confirmation dialog body before deleting a post 317 + /// 318 + /// In en, this message translates to: 319 + /// **'This action cannot be undone.'** 320 + String get dialogDeletePostContent; 321 + 322 + /// Confirmation dialog title before deleting a post 323 + /// 324 + /// In en, this message translates to: 325 + /// **'Delete Post?'** 326 + String get dialogDeletePostTitle; 327 + 328 + /// Confirmation dialog title before deleting a draft 329 + /// 330 + /// In en, this message translates to: 331 + /// **'Delete Draft?'** 332 + String get dialogDeleteDraftTitle; 333 + 334 + /// Confirmation dialog body before leaving unsaved post edits 335 + /// 336 + /// In en, this message translates to: 337 + /// **'You have unsaved edits. Discard them and leave?'** 338 + String get dialogDiscardChangesContent; 339 + 340 + /// Confirmation dialog title before leaving unsaved post edits 341 + /// 342 + /// In en, this message translates to: 343 + /// **'Discard Changes?'** 344 + String get dialogDiscardChangesTitle; 345 + 346 + /// Information dialog body explaining how post editing is implemented 347 + /// 348 + /// In en, this message translates to: 349 + /// **'Lazurite saves edits by deleting and recreating the post record with the same URI. During re-indexing, ranking, counters, and search visibility can shift, and updates may take time to appear everywhere.'** 350 + String get dialogEditAlgorithmContent; 351 + 352 + /// Information dialog title explaining how post editing works 353 + /// 354 + /// In en, this message translates to: 355 + /// **'How Post Editing Works'** 356 + String get dialogEditAlgorithmTitle; 357 + 358 + /// Confirmation dialog body before leaving compose with unsaved content 359 + /// 360 + /// In en, this message translates to: 361 + /// **'You have unsaved content. Would you like to save it as a draft?'** 362 + String get dialogSaveDraftContent; 363 + 364 + /// Confirmation dialog title before leaving compose with unsaved content 365 + /// 366 + /// In en, this message translates to: 367 + /// **'Save Draft?'** 368 + String get dialogSaveDraftTitle; 369 + 220 370 /// Confirmation dialog body before removing a saved account from this device 221 371 /// 222 372 /// In en, this message translates to: ··· 271 421 /// **'Something went wrong'** 272 422 String get errorGenericTitle; 273 423 424 + /// Error title shown when bookmarks cannot load 425 + /// 426 + /// In en, this message translates to: 427 + /// **'Failed to load bookmarks'** 428 + String get errorFailedToLoadBookmarks; 429 + 430 + /// Error title shown when liked posts cannot load 431 + /// 432 + /// In en, this message translates to: 433 + /// **'Failed to load liked posts'** 434 + String get errorFailedToLoadLikedPosts; 435 + 436 + /// Error message shown when liked posts cannot load 437 + /// 438 + /// In en, this message translates to: 439 + /// **'Failed to load liked posts: {error}'** 440 + String errorFailedToLoadLikedPostsDetails(Object error); 441 + 442 + /// Warning message shown when refreshing liked posts fails 443 + /// 444 + /// In en, this message translates to: 445 + /// **'Failed to refresh liked posts: {error}'** 446 + String errorFailedToRefreshLikedPosts(Object error); 447 + 448 + /// Error title shown when trending content cannot load 449 + /// 450 + /// In en, this message translates to: 451 + /// **'Failed to load trending'** 452 + String get errorFailedToLoadTrending; 453 + 454 + /// Error message shown when trending topics cannot load 455 + /// 456 + /// In en, this message translates to: 457 + /// **'Failed to load trending topics: {error}'** 458 + String errorFailedToLoadTrendingTopics(Object error); 459 + 460 + /// Fallback message when an error has no details 461 + /// 462 + /// In en, this message translates to: 463 + /// **'Unknown error'** 464 + String get errorUnknown; 465 + 274 466 /// Snackbar message when a saved account cannot be removed 275 467 /// 276 468 /// In en, this message translates to: ··· 307 499 /// **'Depth {depth}'** 308 500 String formatDepth(int depth); 309 501 502 + /// Fallback liked-post card subtitle showing when a post was liked 503 + /// 504 + /// In en, this message translates to: 505 + /// **'Liked on {date}'** 506 + String formatLikedOn(String date); 507 + 508 + /// Interaction sheet tab label showing like count 509 + /// 510 + /// In en, this message translates to: 511 + /// **'{count, plural, =1{1 Like} other{{count} Likes}}'** 512 + String formatLikesCount(int count); 513 + 514 + /// Message explaining that a post action requires reconnecting 515 + /// 516 + /// In en, this message translates to: 517 + /// **'You are offline. Reconnect to {action}.'** 518 + String formatOfflineReconnectAction(String action); 519 + 520 + /// Post card reply context label showing the parent post author 521 + /// 522 + /// In en, this message translates to: 523 + /// **'Replying to @{handle}'** 524 + String formatReplyingToHandle(String handle); 525 + 526 + /// Interaction sheet tab label showing repost count 527 + /// 528 + /// In en, this message translates to: 529 + /// **'{count, plural, =1{1 Repost} other{{count} Reposts}}'** 530 + String formatRepostsCount(int count); 531 + 532 + /// Fallback saved-post card subtitle showing when a post was saved 533 + /// 534 + /// In en, this message translates to: 535 + /// **'Saved on {date}'** 536 + String formatSavedOn(String date); 537 + 538 + /// Trending topic category subtitle line 539 + /// 540 + /// In en, this message translates to: 541 + /// **'Category: {category}'** 542 + String formatTrendingCategory(String category); 543 + 544 + /// Trending topic post count subtitle line 545 + /// 546 + /// In en, this message translates to: 547 + /// **'{count, plural, =1{1 post} other{{count} posts}}'** 548 + String formatTrendingPostCount(int count); 549 + 550 + /// Post overflow action label to view an author profile 551 + /// 552 + /// In en, this message translates to: 553 + /// **'View @{handle}'** 554 + String formatViewHandle(String handle); 555 + 556 + /// Snackbar message when the image picker fails 557 + /// 558 + /// In en, this message translates to: 559 + /// **'Failed to pick image: {error}'** 560 + String formatComposeFailedToPickImage(Object error); 561 + 562 + /// Snackbar message when the video picker fails 563 + /// 564 + /// In en, this message translates to: 565 + /// **'Failed to pick video: {error}'** 566 + String formatComposeFailedToPickVideo(Object error); 567 + 568 + /// Compose error message when saving edits fails with details 569 + /// 570 + /// In en, this message translates to: 571 + /// **'Failed to save changes: {error}'** 572 + String formatComposeFailedToSaveChanges(Object error); 573 + 574 + /// Compose error message when post submission fails with details 575 + /// 576 + /// In en, this message translates to: 577 + /// **'Failed to submit post: {error}'** 578 + String formatComposeFailedToSubmitPost(Object error); 579 + 580 + /// Compose validation error when an attached image is too large 581 + /// 582 + /// In en, this message translates to: 583 + /// **'Image \"{fileName}\" is {sizeMb} MB - max 1 MB.'** 584 + String formatComposeImageTooLarge(String fileName, String sizeMb); 585 + 586 + /// Compose quote preview label with the quoted author's handle 587 + /// 588 + /// In en, this message translates to: 589 + /// **'Quoting @{handle}'** 590 + String formatComposeQuotingHandle(String handle); 591 + 592 + /// Compose scheduled post pill label 593 + /// 594 + /// In en, this message translates to: 595 + /// **'Scheduled for {dateTime}'** 596 + String formatComposeScheduledFor(String dateTime); 597 + 598 + /// Video attachment status when ready and alt text exists 599 + /// 600 + /// In en, this message translates to: 601 + /// **'Ready - \"{altText}\"'** 602 + String formatComposeVideoReadyWithAltText(String altText); 603 + 604 + /// Compose validation error when a video is too large 605 + /// 606 + /// In en, this message translates to: 607 + /// **'Video is {sizeMb} MB - exceeds the 100 MB limit.'** 608 + String formatComposeVideoTooLarge(String sizeMb); 609 + 610 + /// Compose drafts panel count label 611 + /// 612 + /// In en, this message translates to: 613 + /// **'{count, plural, =1{1 draft} other{{count} drafts}}'** 614 + String formatDraftCount(int count); 615 + 616 + /// Offline action phrase for liking a post 617 + /// 618 + /// In en, this message translates to: 619 + /// **'like this post'** 620 + String get actionLikeThisPost; 621 + 622 + /// Offline action phrase for replying to a post 623 + /// 624 + /// In en, this message translates to: 625 + /// **'reply to this post'** 626 + String get actionReplyToThisPost; 627 + 628 + /// Offline action phrase for reposting a post 629 + /// 630 + /// In en, this message translates to: 631 + /// **'repost this post'** 632 + String get actionRepostThisPost; 633 + 634 + /// Offline action phrase for publishing a post 635 + /// 636 + /// In en, this message translates to: 637 + /// **'publish your post'** 638 + String get actionPublishYourPost; 639 + 310 640 /// About page or settings label 311 641 /// 312 642 /// In en, this message translates to: ··· 409 739 /// **'Bookmarks & Likes'** 410 740 String get labelBookmarksAndLikes; 411 741 742 + /// Tooltip for bookmark actions menu 743 + /// 744 + /// In en, this message translates to: 745 + /// **'Bookmark actions'** 746 + String get labelBookmarkActions; 747 + 748 + /// Fallback saved-post card title 749 + /// 750 + /// In en, this message translates to: 751 + /// **'Bookmarked Post'** 752 + String get labelBookmarkedPost; 753 + 754 + /// Bookmarks tab label 755 + /// 756 + /// In en, this message translates to: 757 + /// **'Bookmarks'** 758 + String get labelBookmarks; 759 + 760 + /// Bluesky source tab label 761 + /// 762 + /// In en, this message translates to: 763 + /// **'Bluesky'** 764 + String get labelBluesky; 765 + 766 + /// Short all-caps label for media alt text controls 767 + /// 768 + /// In en, this message translates to: 769 + /// **'ALT'** 770 + String get labelAlt; 771 + 412 772 /// Snackbar message after clearing cache 413 773 /// 414 774 /// In en, this message translates to: ··· 426 786 /// In en, this message translates to: 427 787 /// **'Clean Follows'** 428 788 String get labelCleanFollows; 789 + 790 + /// Close button tooltip 791 + /// 792 + /// In en, this message translates to: 793 + /// **'Close'** 794 + String get labelClose; 429 795 430 796 /// Settings item for clearing cache 431 797 /// ··· 619 985 /// **'New Post'** 620 986 String get labelNewPost; 621 987 988 + /// Uppercase compact relative time label for the current moment 989 + /// 990 + /// In en, this message translates to: 991 + /// **'NOW'** 992 + String get labelNow; 993 + 622 994 /// Notifications menu label 623 995 /// 624 996 /// In en, this message translates to: ··· 799 1171 /// **'Clear until'** 800 1172 String get labelClearUntil; 801 1173 1174 + /// Bookmark menu item to clear local bookmarks 1175 + /// 1176 + /// In en, this message translates to: 1177 + /// **'Clear local bookmarks'** 1178 + String get labelClearLocalBookmarks; 1179 + 1180 + /// Post overflow menu item to copy a post link 1181 + /// 1182 + /// In en, this message translates to: 1183 + /// **'Copy Link'** 1184 + String get labelCopyLink; 1185 + 1186 + /// Post overflow menu item to delete a post 1187 + /// 1188 + /// In en, this message translates to: 1189 + /// **'Delete Post'** 1190 + String get labelDeletePost; 1191 + 1192 + /// Tooltip for deleting a draft 1193 + /// 1194 + /// In en, this message translates to: 1195 + /// **'Delete draft'** 1196 + String get labelDeleteDraft; 1197 + 1198 + /// Post overflow menu item to edit a post 1199 + /// 1200 + /// In en, this message translates to: 1201 + /// **'Edit Post'** 1202 + String get labelEditPost; 1203 + 1204 + /// Liked posts tab label 1205 + /// 1206 + /// In en, this message translates to: 1207 + /// **'Liked'** 1208 + String get labelLiked; 1209 + 1210 + /// Post interactions sheet section label for users who liked a post 1211 + /// 1212 + /// In en, this message translates to: 1213 + /// **'LIKED BY'** 1214 + String get labelLikedBy; 1215 + 1216 + /// Fallback liked-post card title 1217 + /// 1218 + /// In en, this message translates to: 1219 + /// **'Liked Post'** 1220 + String get labelLikedPost; 1221 + 1222 + /// Tooltip for opening more information 1223 + /// 1224 + /// In en, this message translates to: 1225 + /// **'More info'** 1226 + String get labelMoreInfo; 1227 + 1228 + /// Local saved-post source tab label 1229 + /// 1230 + /// In en, this message translates to: 1231 + /// **'Local'** 1232 + String get labelLocal; 1233 + 1234 + /// Tooltip to open a post 1235 + /// 1236 + /// In en, this message translates to: 1237 + /// **'Open post'** 1238 + String get labelOpenPost; 1239 + 1240 + /// Post action label to quote a post 1241 + /// 1242 + /// In en, this message translates to: 1243 + /// **'Quote Post'** 1244 + String get labelQuotePost; 1245 + 1246 + /// Heading for quote and repost bottom sheet 1247 + /// 1248 + /// In en, this message translates to: 1249 + /// **'QUOTE / REPOSTS'** 1250 + String get labelQuoteReposts; 1251 + 1252 + /// Quotes section title 1253 + /// 1254 + /// In en, this message translates to: 1255 + /// **'Quotes'** 1256 + String get labelQuotes; 1257 + 1258 + /// Post save menu item to remove a cloud bookmark 1259 + /// 1260 + /// In en, this message translates to: 1261 + /// **'Remove from Bluesky'** 1262 + String get labelRemoveFromBluesky; 1263 + 1264 + /// Post save menu item to remove a local bookmark 1265 + /// 1266 + /// In en, this message translates to: 1267 + /// **'Remove local save'** 1268 + String get labelRemoveLocalSave; 1269 + 1270 + /// Post overflow menu item to report a post 1271 + /// 1272 + /// In en, this message translates to: 1273 + /// **'Report Post'** 1274 + String get labelReportPost; 1275 + 1276 + /// Post action label to repost a post 1277 + /// 1278 + /// In en, this message translates to: 1279 + /// **'Repost'** 1280 + String get labelRepost; 1281 + 1282 + /// Post interactions sheet section label for users who reposted a post 1283 + /// 1284 + /// In en, this message translates to: 1285 + /// **'REPOSTED BY'** 1286 + String get labelRepostedBy; 1287 + 1288 + /// Reposts section title 1289 + /// 1290 + /// In en, this message translates to: 1291 + /// **'Reposts'** 1292 + String get labelReposts; 1293 + 1294 + /// Image context menu item to save an image 1295 + /// 1296 + /// In en, this message translates to: 1297 + /// **'Save image'** 1298 + String get labelSaveImage; 1299 + 1300 + /// Post save menu item to create a local bookmark 1301 + /// 1302 + /// In en, this message translates to: 1303 + /// **'Save locally'** 1304 + String get labelSaveLocally; 1305 + 1306 + /// Post save menu item to create a cloud bookmark 1307 + /// 1308 + /// In en, this message translates to: 1309 + /// **'Save to Bluesky'** 1310 + String get labelSaveToBluesky; 1311 + 1312 + /// Compose toolbar schedule button tooltip 1313 + /// 1314 + /// In en, this message translates to: 1315 + /// **'Schedule'** 1316 + String get labelSchedule; 1317 + 1318 + /// Scheduled draft badge label 1319 + /// 1320 + /// In en, this message translates to: 1321 + /// **'Scheduled'** 1322 + String get labelScheduled; 1323 + 802 1324 /// Saved accounts section label on login screen 803 1325 /// 804 1326 /// In en, this message translates to: ··· 816 1338 /// In en, this message translates to: 817 1339 /// **'Semantic Search'** 818 1340 String get labelSemanticSearch; 1341 + 1342 + /// Post overflow menu item to open users who liked a post 1343 + /// 1344 + /// In en, this message translates to: 1345 + /// **'Show Liked Users'** 1346 + String get labelShowLikedUsers; 1347 + 1348 + /// Post overflow menu item to open quote and repost lists 1349 + /// 1350 + /// In en, this message translates to: 1351 + /// **'Show Quote/Repost List'** 1352 + String get labelShowQuoteRepostList; 819 1353 820 1354 /// Settings page title or tooltip 821 1355 /// ··· 877 1411 /// **'Troubleshooting'** 878 1412 String get labelTroubleshooting; 879 1413 1414 + /// Fallback video label 1415 + /// 1416 + /// In en, this message translates to: 1417 + /// **'Video'** 1418 + String get labelVideo; 1419 + 1420 + /// Trending topics section label 1421 + /// 1422 + /// In en, this message translates to: 1423 + /// **'Topics'** 1424 + String get labelTopics; 1425 + 1426 + /// Trending screen title and tooltip label 1427 + /// 1428 + /// In en, this message translates to: 1429 + /// **'Trending'** 1430 + String get labelTrending; 1431 + 1432 + /// Trending suggested topics section label 1433 + /// 1434 + /// In en, this message translates to: 1435 + /// **'Suggested'** 1436 + String get labelSuggested; 1437 + 1438 + /// Suggested follows sheet and menu label 1439 + /// 1440 + /// In en, this message translates to: 1441 + /// **'Suggested Follows'** 1442 + String get labelSuggestedFollows; 1443 + 1444 + /// Post action label to undo a repost 1445 + /// 1446 + /// In en, this message translates to: 1447 + /// **'Unrepost'** 1448 + String get labelUnrepost; 1449 + 880 1450 /// Typeahead provider settings label 881 1451 /// 882 1452 /// In en, this message translates to: ··· 1009 1579 /// **'Manage pinned and saved feeds'** 1010 1580 String get messageFeedsSubtitle; 1011 1581 1582 + /// Snackbar message after copying a post link 1583 + /// 1584 + /// In en, this message translates to: 1585 + /// **'Link copied to clipboard'** 1586 + String get messageLinkCopiedToClipboard; 1587 + 1588 + /// Loading message while trending topics load 1589 + /// 1590 + /// In en, this message translates to: 1591 + /// **'Loading trending topics'** 1592 + String get messageLoadingTrendingTopics; 1593 + 1012 1594 /// Developer setting subtitle for forced 401 1013 1595 /// 1014 1596 /// In en, this message translates to: ··· 1021 1603 /// **'Manage semantic search from Bookmarks & Likes -> Search'** 1022 1604 String get messageManageSemanticSearchSubtitle; 1023 1605 1606 + /// Trending banner shown when supplemental metadata cannot be loaded 1607 + /// 1608 + /// In en, this message translates to: 1609 + /// **'Metadata temporarily unavailable'** 1610 + String get messageMetadataTemporarilyUnavailable; 1611 + 1024 1612 /// Moderation overlay description when content cannot be revealed 1025 1613 /// 1026 1614 /// In en, this message translates to: ··· 1039 1627 /// **'Moderation/ranking can differ by provider. Verify health and recent fallback state.'** 1040 1628 String get messageProviderDiagnosticsSubtitle; 1041 1629 1630 + /// Message shown when liked posts dependencies are unavailable 1631 + /// 1632 + /// In en, this message translates to: 1633 + /// **'Liked posts are unavailable right now.'** 1634 + String get messageLikedPostsUnavailable; 1635 + 1636 + /// Snackbar message after post edits are saved 1637 + /// 1638 + /// In en, this message translates to: 1639 + /// **'Changes saved.'** 1640 + String get messageChangesSaved; 1641 + 1642 + /// Tooltip for adding video alt text 1643 + /// 1644 + /// In en, this message translates to: 1645 + /// **'Add alt text'** 1646 + String get messageComposeAddAltText; 1647 + 1648 + /// Compose toolbar add image tooltip 1649 + /// 1650 + /// In en, this message translates to: 1651 + /// **'Add image'** 1652 + String get messageComposeAddImage; 1653 + 1654 + /// Compose toolbar add video tooltip 1655 + /// 1656 + /// In en, this message translates to: 1657 + /// **'Add video'** 1658 + String get messageComposeAddVideo; 1659 + 1660 + /// Tooltip for clearing scheduled compose time 1661 + /// 1662 + /// In en, this message translates to: 1663 + /// **'Clear scheduled time'** 1664 + String get messageComposeClearScheduledTime; 1665 + 1666 + /// Image alt text field placeholder 1667 + /// 1668 + /// In en, this message translates to: 1669 + /// **'Describe the image'** 1670 + String get messageComposeDescribeImage; 1671 + 1672 + /// Video alt text field placeholder 1673 + /// 1674 + /// In en, this message translates to: 1675 + /// **'Describe the video'** 1676 + String get messageComposeDescribeVideo; 1677 + 1678 + /// Snackbar message after saving a draft 1679 + /// 1680 + /// In en, this message translates to: 1681 + /// **'Draft saved'** 1682 + String get messageComposeDraftSaved; 1683 + 1684 + /// Compose drafts panel title and toolbar tooltip 1685 + /// 1686 + /// In en, this message translates to: 1687 + /// **'Drafts'** 1688 + String get messageComposeDrafts; 1689 + 1690 + /// Compose edit mode notice banner 1691 + /// 1692 + /// In en, this message translates to: 1693 + /// **'Edits are saved by replacing the record while keeping this post URI. Ranking, counts, and visibility may shift while networks re-index.'** 1694 + String get messageComposeEditNotice; 1695 + 1696 + /// Image alt text dialog title 1697 + /// 1698 + /// In en, this message translates to: 1699 + /// **'Alt text'** 1700 + String get messageComposeImageAltTextTitle; 1701 + 1702 + /// Compose validation message when too many images are attached 1703 + /// 1704 + /// In en, this message translates to: 1705 + /// **'Maximum 4 images allowed'** 1706 + String get messageComposeImageMaxCount; 1707 + 1708 + /// Compose validation message for unsupported image extensions 1709 + /// 1710 + /// In en, this message translates to: 1711 + /// **'Image must be JPEG, PNG, or WebP'** 1712 + String get messageComposeImageMustBeJpegPngWebp; 1713 + 1714 + /// Compose validation message for image picker size validation 1715 + /// 1716 + /// In en, this message translates to: 1717 + /// **'Image must be smaller than 1MB'** 1718 + String get messageComposeImageMustBeUnder1Mb; 1719 + 1720 + /// Empty state text in compose drafts panel 1721 + /// 1722 + /// In en, this message translates to: 1723 + /// **'No drafts saved'** 1724 + String get messageComposeNoDraftsSaved; 1725 + 1726 + /// Fallback draft content label when a draft has no text 1727 + /// 1728 + /// In en, this message translates to: 1729 + /// **'(No text)'** 1730 + String get messageComposeNoText; 1731 + 1732 + /// Compose text field placeholder 1733 + /// 1734 + /// In en, this message translates to: 1735 + /// **'What\'\'s on your mind?'** 1736 + String get messageComposePlaceholder; 1737 + 1738 + /// Video alt text preview unavailable message 1739 + /// 1740 + /// In en, this message translates to: 1741 + /// **'Preview unavailable'** 1742 + String get messageComposePreviewUnavailable; 1743 + 1744 + /// Compose quote preview label without an author handle 1745 + /// 1746 + /// In en, this message translates to: 1747 + /// **'Quoting post'** 1748 + String get messageComposeQuotingPost; 1749 + 1750 + /// Compose validation message when a video cannot be added because other media exists 1751 + /// 1752 + /// In en, this message translates to: 1753 + /// **'Remove existing media before adding a video'** 1754 + String get messageComposeRemoveExistingMediaBeforeVideo; 1755 + 1756 + /// Tooltip for removing an image attachment 1757 + /// 1758 + /// In en, this message translates to: 1759 + /// **'Remove image'** 1760 + String get messageComposeRemoveImage; 1761 + 1762 + /// Tooltip for removing a quoted post from compose 1763 + /// 1764 + /// In en, this message translates to: 1765 + /// **'Remove quoted post'** 1766 + String get messageComposeRemoveQuotedPost; 1767 + 1768 + /// Compose toolbar save draft tooltip 1769 + /// 1770 + /// In en, this message translates to: 1771 + /// **'Save draft'** 1772 + String get messageComposeSaveDraft; 1773 + 1774 + /// Video alt text dialog title 1775 + /// 1776 + /// In en, this message translates to: 1777 + /// **'Video alt text'** 1778 + String get messageComposeVideoAltTextTitle; 1779 + 1780 + /// Video attachment status while checking upload limits 1781 + /// 1782 + /// In en, this message translates to: 1783 + /// **'Checking upload limits...'** 1784 + String get messageVideoCheckingUploadLimits; 1785 + 1786 + /// Video upload validation message when the daily limit is reached 1787 + /// 1788 + /// In en, this message translates to: 1789 + /// **'Daily video upload limit reached.'** 1790 + String get messageVideoDailyUploadLimitReached; 1791 + 1792 + /// Video attachment processing status 1793 + /// 1794 + /// In en, this message translates to: 1795 + /// **'Processing...'** 1796 + String get messageVideoProcessing; 1797 + 1798 + /// Video attachment error when processing fails 1799 + /// 1800 + /// In en, this message translates to: 1801 + /// **'Video processing failed.'** 1802 + String get messageVideoProcessingFailed; 1803 + 1804 + /// Video attachment error when processing times out 1805 + /// 1806 + /// In en, this message translates to: 1807 + /// **'Video processing timed out.'** 1808 + String get messageVideoProcessingTimedOut; 1809 + 1810 + /// Video attachment ready status 1811 + /// 1812 + /// In en, this message translates to: 1813 + /// **'Ready'** 1814 + String get messageVideoReady; 1815 + 1816 + /// Video attachment ready-to-upload status 1817 + /// 1818 + /// In en, this message translates to: 1819 + /// **'Ready to upload'** 1820 + String get messageVideoReadyToUpload; 1821 + 1822 + /// Video upload generic failure message 1823 + /// 1824 + /// In en, this message translates to: 1825 + /// **'Upload failed - please try again.'** 1826 + String get messageVideoUploadFailed; 1827 + 1828 + /// Video attachment uploading status 1829 + /// 1830 + /// In en, this message translates to: 1831 + /// **'Uploading...'** 1832 + String get messageVideoUploading; 1833 + 1834 + /// Compose edit error when the post changed remotely 1835 + /// 1836 + /// In en, this message translates to: 1837 + /// **'This post was changed elsewhere. Reopen it and try editing again.'** 1838 + String get errorComposeChangedElsewhere; 1839 + 1840 + /// Compose edit error when an edit cannot be confirmed 1841 + /// 1842 + /// In en, this message translates to: 1843 + /// **'Edit was submitted but could not be confirmed yet. Please reopen the post and verify.'** 1844 + String get errorComposeCouldNotConfirmEdit; 1845 + 1846 + /// Compose edit recovery error when save and recovery status are unknown 1847 + /// 1848 + /// In en, this message translates to: 1849 + /// **'Could not save changes and we could not confirm recovery. Reopen the thread and verify the post.'** 1850 + String get errorComposeCouldNotSaveAndConfirmRecovery; 1851 + 1852 + /// Compose edit validation error when edit context is missing 1853 + /// 1854 + /// In en, this message translates to: 1855 + /// **'Edit context is missing. Please reopen the editor and try again.'** 1856 + String get errorComposeEditContextMissing; 1857 + 1858 + /// Compose error when creating a post fails 1859 + /// 1860 + /// In en, this message translates to: 1861 + /// **'Failed to create post. Please try again.'** 1862 + String get errorComposeFailedToCreatePost; 1863 + 1864 + /// Compose error when saving post edits fails 1865 + /// 1866 + /// In en, this message translates to: 1867 + /// **'Failed to save changes. Please try again.'** 1868 + String get errorComposeFailedToSaveChanges; 1869 + 1870 + /// Compose error when image upload fails 1871 + /// 1872 + /// In en, this message translates to: 1873 + /// **'Failed to upload image. Please try again.'** 1874 + String get errorComposeFailedToUploadImage; 1875 + 1876 + /// Compose error when an attached image file is missing 1877 + /// 1878 + /// In en, this message translates to: 1879 + /// **'Image file not found. Please re-attach and try again.'** 1880 + String get errorComposeImageFileNotFound; 1881 + 1882 + /// Compose error when submission fails and is saved as a draft 1883 + /// 1884 + /// In en, this message translates to: 1885 + /// **'Network error - post saved as draft.'** 1886 + String get errorComposeNetworkSavedAsDraft; 1887 + 1888 + /// Compose edit error when the original post was restored after failure 1889 + /// 1890 + /// In en, this message translates to: 1891 + /// **'Could not save changes. Your original post was restored.'** 1892 + String get errorComposeOriginalPostRestored; 1893 + 1894 + /// Compose validation error for unsupported image bytes 1895 + /// 1896 + /// In en, this message translates to: 1897 + /// **'Unsupported image format. Use JPEG, PNG, or WebP.'** 1898 + String get errorComposeUnsupportedImageFormat; 1899 + 1900 + /// Empty state title when there are no bookmarks 1901 + /// 1902 + /// In en, this message translates to: 1903 + /// **'No bookmarks'** 1904 + String get messageNoBookmarks; 1905 + 1906 + /// Empty state subtitle when there are no bookmarks 1907 + /// 1908 + /// In en, this message translates to: 1909 + /// **'Posts you bookmark will appear here'** 1910 + String get messageNoBookmarksSubtitle; 1911 + 1912 + /// Empty state title when a bookmark source tab is empty 1913 + /// 1914 + /// In en, this message translates to: 1915 + /// **'No bookmarks in this source'** 1916 + String get messageNoBookmarksInSource; 1917 + 1918 + /// Empty state subtitle when a bookmark source tab is empty 1919 + /// 1920 + /// In en, this message translates to: 1921 + /// **'Try switching tabs or saving posts to this source'** 1922 + String get messageNoBookmarksInSourceSubtitle; 1923 + 1924 + /// Empty state message when a post has no likes or reposts 1925 + /// 1926 + /// In en, this message translates to: 1927 + /// **'No interactions yet'** 1928 + String get messageNoInteractionsYet; 1929 + 1930 + /// Empty state title when there are no liked posts 1931 + /// 1932 + /// In en, this message translates to: 1933 + /// **'No liked posts'** 1934 + String get messageNoLikedPosts; 1935 + 1936 + /// Empty state subtitle when there are no liked posts 1937 + /// 1938 + /// In en, this message translates to: 1939 + /// **'Posts you like will appear here after sync'** 1940 + String get messageNoLikedPostsSubtitle; 1941 + 1942 + /// Empty state when a profile has no liked posts 1943 + /// 1944 + /// In en, this message translates to: 1945 + /// **'No liked posts yet'** 1946 + String get messageNoLikedPostsYet; 1947 + 1948 + /// Empty state message when a post has no quote posts 1949 + /// 1950 + /// In en, this message translates to: 1951 + /// **'No quotes yet'** 1952 + String get messageNoQuotesYet; 1953 + 1954 + /// Empty state message when a post has no reposts 1955 + /// 1956 + /// In en, this message translates to: 1957 + /// **'No reposts yet'** 1958 + String get messageNoRepostsYet; 1959 + 1960 + /// Empty state message when there are no trending topics 1961 + /// 1962 + /// In en, this message translates to: 1963 + /// **'No trending topics right now'** 1964 + String get messageNoTrendingTopicsRightNow; 1965 + 1966 + /// Snackbar message after deleting a post 1967 + /// 1968 + /// In en, this message translates to: 1969 + /// **'Post deleted'** 1970 + String get messagePostDeleted; 1971 + 1972 + /// Post action subtitle for quote post 1973 + /// 1974 + /// In en, this message translates to: 1975 + /// **'Quote this post with your own text'** 1976 + String get messageQuotePostSubtitle; 1977 + 1978 + /// Quoted embed unavailable message for blocked quoted posts 1979 + /// 1980 + /// In en, this message translates to: 1981 + /// **'Quoted post is blocked'** 1982 + String get messageQuotedPostBlocked; 1983 + 1984 + /// Quoted embed unavailable message for missing quoted posts 1985 + /// 1986 + /// In en, this message translates to: 1987 + /// **'Quoted post not found'** 1988 + String get messageQuotedPostNotFound; 1989 + 1990 + /// Quoted embed unavailable message for detached or unavailable quoted posts 1991 + /// 1992 + /// In en, this message translates to: 1993 + /// **'Quoted post is unavailable'** 1994 + String get messageQuotedPostUnavailable; 1995 + 1996 + /// Post action subtitle for removing a repost 1997 + /// 1998 + /// In en, this message translates to: 1999 + /// **'Remove this repost'** 2000 + String get messageRemoveRepostSubtitle; 2001 + 2002 + /// Post card reply context label when parent post details are unavailable 2003 + /// 2004 + /// In en, this message translates to: 2005 + /// **'Reply in a thread'** 2006 + String get messageReplyInThread; 2007 + 2008 + /// Compose reply banner prefix before the replied-to handle 2009 + /// 2010 + /// In en, this message translates to: 2011 + /// **'Replying to'** 2012 + String get messageReplyingTo; 2013 + 2014 + /// Post action subtitle for reposting a post 2015 + /// 2016 + /// In en, this message translates to: 2017 + /// **'Share this post'** 2018 + String get messageShareThisPostSubtitle; 2019 + 2020 + /// Post overflow subtitle for viewing users who liked a post 2021 + /// 2022 + /// In en, this message translates to: 2023 + /// **'View who liked this post'** 2024 + String get messageShowLikedUsersSubtitle; 2025 + 2026 + /// Post overflow subtitle for viewing quotes and reposts 2027 + /// 2028 + /// In en, this message translates to: 2029 + /// **'View quote posts and expand reposts'** 2030 + String get messageShowQuoteRepostListSubtitle; 2031 + 1042 2032 /// Settings subtitle for refreshing provider health 1043 2033 /// 1044 2034 /// In en, this message translates to: ··· 1123 2113 /// **'Search people'** 1124 2114 String get messageSearchPeoplePlaceholder; 1125 2115 2116 + /// Search field placeholder when adding list members 2117 + /// 2118 + /// In en, this message translates to: 2119 + /// **'Search for people'** 2120 + String get messageSearchForPeoplePlaceholder; 2121 + 1126 2122 /// Search feeds field placeholder 1127 2123 /// 1128 2124 /// In en, this message translates to: ··· 1177 2173 /// **'Handle or DID'** 1178 2174 String get promptHandleOrDid; 1179 2175 2176 + /// Button label to add a feed 2177 + /// 2178 + /// In en, this message translates to: 2179 + /// **'Add feed'** 2180 + String get buttonAddFeed; 2181 + 2182 + /// Button label to add members to a list 2183 + /// 2184 + /// In en, this message translates to: 2185 + /// **'Add members'** 2186 + String get buttonAddMembers; 2187 + 2188 + /// Button label to block an account 2189 + /// 2190 + /// In en, this message translates to: 2191 + /// **'Block'** 2192 + String get buttonBlock; 2193 + 2194 + /// Create button label 2195 + /// 2196 + /// In en, this message translates to: 2197 + /// **'Create'** 2198 + String get buttonCreate; 2199 + 2200 + /// Edit button label 2201 + /// 2202 + /// In en, this message translates to: 2203 + /// **'Edit'** 2204 + String get buttonEdit; 2205 + 2206 + /// Button label to follow an account 2207 + /// 2208 + /// In en, this message translates to: 2209 + /// **'Follow'** 2210 + String get buttonFollow; 2211 + 2212 + /// Button label to follow all starter pack members 2213 + /// 2214 + /// In en, this message translates to: 2215 + /// **'Follow all'** 2216 + String get buttonFollowAll; 2217 + 2218 + /// Button label showing that an account is followed 2219 + /// 2220 + /// In en, this message translates to: 2221 + /// **'Following'** 2222 + String get buttonFollowing; 2223 + 2224 + /// Button label while following all starter pack members 2225 + /// 2226 + /// In en, this message translates to: 2227 + /// **'Following…'** 2228 + String get buttonFollowingInProgress; 2229 + 2230 + /// Button label to load more results 2231 + /// 2232 + /// In en, this message translates to: 2233 + /// **'Load more'** 2234 + String get buttonLoadMore; 2235 + 2236 + /// Button label to mute an account or list 2237 + /// 2238 + /// In en, this message translates to: 2239 + /// **'Mute'** 2240 + String get buttonMute; 2241 + 2242 + /// Button label to start a follow audit scan 2243 + /// 2244 + /// In en, this message translates to: 2245 + /// **'Scan'** 2246 + String get buttonScan; 2247 + 2248 + /// Button label to view all items 2249 + /// 2250 + /// In en, this message translates to: 2251 + /// **'See all'** 2252 + String get buttonSeeAll; 2253 + 2254 + /// Button label to reveal blocked-by accounts 2255 + /// 2256 + /// In en, this message translates to: 2257 + /// **'Show accounts'** 2258 + String get buttonShowAccounts; 2259 + 2260 + /// Button label to submit a moderation report 2261 + /// 2262 + /// In en, this message translates to: 2263 + /// **'Submit Report'** 2264 + String get buttonSubmitReport; 2265 + 2266 + /// Button label to unblock an account 2267 + /// 2268 + /// In en, this message translates to: 2269 + /// **'Unblock'** 2270 + String get buttonUnblock; 2271 + 2272 + /// Button label to unfollow an account 2273 + /// 2274 + /// In en, this message translates to: 2275 + /// **'Unfollow'** 2276 + String get buttonUnfollow; 2277 + 2278 + /// Button label to unfollow selected audit results 2279 + /// 2280 + /// In en, this message translates to: 2281 + /// **'Unfollow Selected ({count})'** 2282 + String buttonUnfollowSelected(int count); 2283 + 2284 + /// Button label to unmute an account or list 2285 + /// 2286 + /// In en, this message translates to: 2287 + /// **'Unmute'** 2288 + String get buttonUnmute; 2289 + 2290 + /// Confirmation dialog body before blocking an account 2291 + /// 2292 + /// In en, this message translates to: 2293 + /// **'They will not be able to see your posts or interact with you. They will not be notified that you blocked them.'** 2294 + String get dialogBlockAccountContent; 2295 + 2296 + /// Confirmation dialog title before blocking an account 2297 + /// 2298 + /// In en, this message translates to: 2299 + /// **'Block Account?'** 2300 + String get dialogBlockAccountTitle; 2301 + 2302 + /// Confirmation dialog title before deleting a list 2303 + /// 2304 + /// In en, this message translates to: 2305 + /// **'Delete list?'** 2306 + String get dialogDeleteListTitle; 2307 + 2308 + /// Confirmation dialog body before deleting a starter pack 2309 + /// 2310 + /// In en, this message translates to: 2311 + /// **'This will permanently delete this starter pack and its backing list. This cannot be undone.'** 2312 + String get dialogDeleteStarterPackContent; 2313 + 2314 + /// Confirmation dialog title before deleting a starter pack 2315 + /// 2316 + /// In en, this message translates to: 2317 + /// **'Delete starter pack'** 2318 + String get dialogDeleteStarterPackTitle; 2319 + 2320 + /// Confirmation dialog body before muting an account 2321 + /// 2322 + /// In en, this message translates to: 2323 + /// **'You will no longer see their posts or receive notifications from them.'** 2324 + String get dialogMuteAccountContent; 2325 + 2326 + /// Confirmation dialog title before muting an account 2327 + /// 2328 + /// In en, this message translates to: 2329 + /// **'Mute Account?'** 2330 + String get dialogMuteAccountTitle; 2331 + 2332 + /// Confirmation dialog body before unblocking an account 2333 + /// 2334 + /// In en, this message translates to: 2335 + /// **'They will be able to see your posts and interact with you again.'** 2336 + String get dialogUnblockAccountContent; 2337 + 2338 + /// Confirmation dialog title before unblocking an account 2339 + /// 2340 + /// In en, this message translates to: 2341 + /// **'Unblock Account?'** 2342 + String get dialogUnblockAccountTitle; 2343 + 2344 + /// Confirmation dialog body before unfollowing an account 2345 + /// 2346 + /// In en, this message translates to: 2347 + /// **'You will no longer see their posts in your feed.'** 2348 + String get dialogUnfollowAccountContent; 2349 + 2350 + /// Confirmation dialog title before unfollowing an account 2351 + /// 2352 + /// In en, this message translates to: 2353 + /// **'Unfollow?'** 2354 + String get dialogUnfollowAccountTitle; 2355 + 2356 + /// Confirmation dialog body before unmuting an account 2357 + /// 2358 + /// In en, this message translates to: 2359 + /// **'You will see their posts and receive notifications again.'** 2360 + String get dialogUnmuteAccountContent; 2361 + 2362 + /// Confirmation dialog title before unmuting an account 2363 + /// 2364 + /// In en, this message translates to: 2365 + /// **'Unmute Account?'** 2366 + String get dialogUnmuteAccountTitle; 2367 + 2368 + /// Error message when starter pack creation fails 2369 + /// 2370 + /// In en, this message translates to: 2371 + /// **'Failed to create starter pack'** 2372 + String get errorFailedToCreateStarterPack; 2373 + 2374 + /// Error message when accounts cannot load 2375 + /// 2376 + /// In en, this message translates to: 2377 + /// **'Failed to load accounts'** 2378 + String get errorFailedToLoadAccounts; 2379 + 2380 + /// Error message when a list feed cannot load 2381 + /// 2382 + /// In en, this message translates to: 2383 + /// **'Failed to load feed'** 2384 + String get errorFailedToLoadFeed; 2385 + 2386 + /// Error message when feed picker suggestions cannot load 2387 + /// 2388 + /// In en, this message translates to: 2389 + /// **'Failed to load feeds'** 2390 + String get errorFailedToLoadFeeds; 2391 + 2392 + /// Error message when a list cannot load 2393 + /// 2394 + /// In en, this message translates to: 2395 + /// **'Failed to load list'** 2396 + String get errorFailedToLoadList; 2397 + 2398 + /// Error message when lists cannot load 2399 + /// 2400 + /// In en, this message translates to: 2401 + /// **'Failed to load lists'** 2402 + String get errorFailedToLoadLists; 2403 + 2404 + /// Error message when list members cannot load 2405 + /// 2406 + /// In en, this message translates to: 2407 + /// **'Failed to load members'** 2408 + String get errorFailedToLoadMembers; 2409 + 2410 + /// Error message when loading more results fails 2411 + /// 2412 + /// In en, this message translates to: 2413 + /// **'Failed to load more'** 2414 + String get errorFailedToLoadMore; 2415 + 2416 + /// Error message when posts cannot load 2417 + /// 2418 + /// In en, this message translates to: 2419 + /// **'Failed to load posts'** 2420 + String get errorFailedToLoadPosts; 2421 + 2422 + /// Error title when a profile cannot load 2423 + /// 2424 + /// In en, this message translates to: 2425 + /// **'Unable to load profile'** 2426 + String get errorFailedToLoadProfile; 2427 + 2428 + /// Error message when a starter pack cannot load 2429 + /// 2430 + /// In en, this message translates to: 2431 + /// **'Failed to load starter pack'** 2432 + String get errorFailedToLoadStarterPack; 2433 + 2434 + /// Error message when starter packs cannot load 2435 + /// 2436 + /// In en, this message translates to: 2437 + /// **'Failed to load starter packs'** 2438 + String get errorFailedToLoadStarterPacks; 2439 + 2440 + /// Error title when suggested follows cannot load 2441 + /// 2442 + /// In en, this message translates to: 2443 + /// **'Failed to load suggestions'** 2444 + String get errorFailedToLoadSuggestions; 2445 + 2446 + /// Error message when follow audit fails 2447 + /// 2448 + /// In en, this message translates to: 2449 + /// **'Failed to complete follow audit.'** 2450 + String get errorFollowAuditFailed; 2451 + 2452 + /// Validation error when a profile image is too large 2453 + /// 2454 + /// In en, this message translates to: 2455 + /// **'Image must be smaller than 1MB'** 2456 + String get errorImageTooLarge; 2457 + 2458 + /// Validation error when profile image file type is unsupported 2459 + /// 2460 + /// In en, this message translates to: 2461 + /// **'Use a JPEG or PNG image'** 2462 + String get errorInvalidProfileImageType; 2463 + 2464 + /// Error when a selected profile image cannot be read 2465 + /// 2466 + /// In en, this message translates to: 2467 + /// **'Unable to read selected image'** 2468 + String get errorProfileImageReadFailed; 2469 + 2470 + /// Report submission failure dialog body 2471 + /// 2472 + /// In en, this message translates to: 2473 + /// **'Unable to submit your report. Please try again later.'** 2474 + String get errorReportFailed; 2475 + 2476 + /// Report submission failure dialog title 2477 + /// 2478 + /// In en, this message translates to: 2479 + /// **'Report Failed'** 2480 + String get errorReportFailedTitle; 2481 + 2482 + /// Error title when a profile connection tab cannot load 2483 + /// 2484 + /// In en, this message translates to: 2485 + /// **'Unable to load {tab}'** 2486 + String errorUnableToLoadConnections(String tab); 2487 + 2488 + /// Snackbar message when profile update fails 2489 + /// 2490 + /// In en, this message translates to: 2491 + /// **'Unable to update profile'** 2492 + String get errorUnableToUpdateProfile; 2493 + 2494 + /// Count of accounts 2495 + /// 2496 + /// In en, this message translates to: 2497 + /// **'{count, plural, =1{1 account} other{{count} accounts}}'** 2498 + String formatAccountCount(int count); 2499 + 2500 + /// Message when blocked-by account count exists but profiles are unavailable 2501 + /// 2502 + /// In en, this message translates to: 2503 + /// **'Found {count, plural, =1{1 blocked-by account} other{{count} blocked-by accounts}}, but public Bluesky profile details could not be loaded.'** 2504 + String formatBlockedByAccountsUnavailable(int count); 2505 + 2506 + /// Loading message for a profile connection tab 2507 + /// 2508 + /// In en, this message translates to: 2509 + /// **'Loading {tab}...'** 2510 + String formatConnectionsLoading(String tab); 2511 + 2512 + /// Empty search message for a profile connection tab 2513 + /// 2514 + /// In en, this message translates to: 2515 + /// **'No {tab} match \"{query}\"'** 2516 + String formatConnectionsNoMatches(String tab, String query); 2517 + 2518 + /// Empty message for a profile connection tab 2519 + /// 2520 + /// In en, this message translates to: 2521 + /// **'No {tab} found'** 2522 + String formatConnectionsNoneFound(String tab); 2523 + 2524 + /// Search progress message for profile connections 2525 + /// 2526 + /// In en, this message translates to: 2527 + /// **'Searching {count} accounts...'** 2528 + String formatConnectionsSearching(int count); 2529 + 2530 + /// Completed search progress message for profile connections 2531 + /// 2532 + /// In en, this message translates to: 2533 + /// **'Searched {count} accounts'** 2534 + String formatConnectionsSearched(int count); 2535 + 2536 + /// Stopped search progress message for profile connections 2537 + /// 2538 + /// In en, this message translates to: 2539 + /// **'Search stopped after {count} accounts'** 2540 + String formatConnectionsSearchStopped(int count); 2541 + 2542 + /// Follow audit classifying progress label 2543 + /// 2544 + /// In en, this message translates to: 2545 + /// **'Classifying: {progress}/{total}'** 2546 + String formatClassifyingProgress(int progress, int total); 2547 + 2548 + /// Snackbar message after copying a DID 2549 + /// 2550 + /// In en, this message translates to: 2551 + /// **'DID copied to clipboard'** 2552 + String get formatDidCopied; 2553 + 2554 + /// Follow audit fetching progress label 2555 + /// 2556 + /// In en, this message translates to: 2557 + /// **'Fetching follows: {progress}/{total}'** 2558 + String formatFetchingFollowsProgress(int progress, int total); 2559 + 2560 + /// Snackbar message after following starter pack members 2561 + /// 2562 + /// In en, this message translates to: 2563 + /// **'Followed {count, plural, =1{1 member} other{{count} members}}'** 2564 + String formatFollowedMemberCount(int count); 2565 + 2566 + /// Follow audit summary after scanning follows 2567 + /// 2568 + /// In en, this message translates to: 2569 + /// **'{count, plural, =1{1 follow scanned for problematic accounts} other{{count} follows scanned for problematic accounts}}'** 2570 + String formatFollowsScanned(int count); 2571 + 2572 + /// Tooltip to hide a follow audit status 2573 + /// 2574 + /// In en, this message translates to: 2575 + /// **'Hide {status}'** 2576 + String formatHideStatus(String status); 2577 + 2578 + /// Profile joined date label 2579 + /// 2580 + /// In en, this message translates to: 2581 + /// **'Joined {date}'** 2582 + String formatJoinedDate(String date); 2583 + 2584 + /// Profile card joined relative time label 2585 + /// 2586 + /// In en, this message translates to: 2587 + /// **'Joined {relativeTime}'** 2588 + String formatJoinedRelative(String relativeTime); 2589 + 2590 + /// List creator attribution label 2591 + /// 2592 + /// In en, this message translates to: 2593 + /// **'by @{handle}'** 2594 + String formatListByHandle(String handle); 2595 + 2596 + /// Count of list or starter pack members 2597 + /// 2598 + /// In en, this message translates to: 2599 + /// **'{count, plural, =1{1 member} other{{count} members}}'** 2600 + String formatMemberCount(int count); 2601 + 2602 + /// Report dialog title for a target handle 2603 + /// 2604 + /// In en, this message translates to: 2605 + /// **'{title} by @{handle}'** 2606 + String formatProfileReportTitle(String title, String handle); 2607 + 2608 + /// Profile edit validation message for a text length limit 2609 + /// 2610 + /// In en, this message translates to: 2611 + /// **'{label} must be {count} characters or fewer'** 2612 + String formatProfileTextLimit(String label, int count); 2613 + 2614 + /// Profile edit validation message for byte length 2615 + /// 2616 + /// In en, this message translates to: 2617 + /// **'{label} is too long'** 2618 + String formatProfileTextTooLong(String label); 2619 + 2620 + /// Follow audit warning when some profiles failed to load 2621 + /// 2622 + /// In en, this message translates to: 2623 + /// **'{count, plural, =1{1 profile could not be loaded.} other{{count} profiles could not be loaded.}}'** 2624 + String formatProfilesFailedToLoad(int count); 2625 + 2626 + /// Report submission success dialog body 2627 + /// 2628 + /// In en, this message translates to: 2629 + /// **'Thank you. Your report (ID: {reportId}) has been submitted.'** 2630 + String formatReportSubmitted(String reportId); 2631 + 2632 + /// Follow audit selected count footer 2633 + /// 2634 + /// In en, this message translates to: 2635 + /// **'Selected: {selected}/{total}'** 2636 + String formatSelectedCount(int selected, int total); 2637 + 2638 + /// Tooltip to show a follow audit status 2639 + /// 2640 + /// In en, this message translates to: 2641 + /// **'Show {status}'** 2642 + String formatShowStatus(String status); 2643 + 2644 + /// Title for unavailable accounts card 2645 + /// 2646 + /// In en, this message translates to: 2647 + /// **'Unavailable accounts ({count})'** 2648 + String formatUnavailableAccounts(int count); 2649 + 2650 + /// Follow audit completion message 2651 + /// 2652 + /// In en, this message translates to: 2653 + /// **'Unfollowed {count} account(s)'** 2654 + String formatUnfollowedAccounts(int count); 2655 + 2656 + /// Helper text for required character-limited fields 2657 + /// 2658 + /// In en, this message translates to: 2659 + /// **'Required, max {count} characters'** 2660 + String formatValidationRequiredMaxCharacters(int count); 2661 + 2662 + /// Action label to add an account to a list 2663 + /// 2664 + /// In en, this message translates to: 2665 + /// **'Add to list'** 2666 + String get labelAddToList; 2667 + 2668 + /// Follow audit screen title 2669 + /// 2670 + /// In en, this message translates to: 2671 + /// **'Audit Followers'** 2672 + String get labelAuditFollowers; 2673 + 2674 + /// Profile banner image button label 2675 + /// 2676 + /// In en, this message translates to: 2677 + /// **'Banner'** 2678 + String get labelBanner; 2679 + 2680 + /// List action label to block accounts via a moderation list 2681 + /// 2682 + /// In en, this message translates to: 2683 + /// **'Block via list'** 2684 + String get labelBlockViaList; 2685 + 2686 + /// Profile context tab label for accounts that blocked the user 2687 + /// 2688 + /// In en, this message translates to: 2689 + /// **'Blocked By'** 2690 + String get labelBlockedBy; 2691 + 2692 + /// Profile context tab label for accounts the user is blocking 2693 + /// 2694 + /// In en, this message translates to: 2695 + /// **'Blocking'** 2696 + String get labelBlocking; 2697 + 2698 + /// Profile connections screen title 2699 + /// 2700 + /// In en, this message translates to: 2701 + /// **'Connections'** 2702 + String get labelConnections; 2703 + 2704 + /// Profile action label to copy DID 2705 + /// 2706 + /// In en, this message translates to: 2707 + /// **'Copy DID'** 2708 + String get labelCopyDid; 2709 + 2710 + /// Create list dialog title 2711 + /// 2712 + /// In en, this message translates to: 2713 + /// **'Create list'** 2714 + String get labelCreateList; 2715 + 2716 + /// Tooltip to create a starter pack 2717 + /// 2718 + /// In en, this message translates to: 2719 + /// **'Create starter pack'** 2720 + String get labelCreateStarterPack; 2721 + 2722 + /// Short curation list badge label 2723 + /// 2724 + /// In en, this message translates to: 2725 + /// **'CURATE'** 2726 + String get labelCurateShort; 2727 + 2728 + /// List members section heading 2729 + /// 2730 + /// In en, this message translates to: 2731 + /// **'Current Members'** 2732 + String get labelCurrentMembers; 2733 + 2734 + /// Profile context list section heading 2735 + /// 2736 + /// In en, this message translates to: 2737 + /// **'Curation Lists'** 2738 + String get labelCurationLists; 2739 + 2740 + /// Description field label 2741 + /// 2742 + /// In en, this message translates to: 2743 + /// **'Description'** 2744 + String get labelDescription; 2745 + 2746 + /// Optional description field label 2747 + /// 2748 + /// In en, this message translates to: 2749 + /// **'Description (optional)'** 2750 + String get labelDescriptionOptional; 2751 + 2752 + /// Profile display name field label 2753 + /// 2754 + /// In en, this message translates to: 2755 + /// **'Display name'** 2756 + String get labelDisplayName; 2757 + 2758 + /// Edit list dialog or action title 2759 + /// 2760 + /// In en, this message translates to: 2761 + /// **'Edit list'** 2762 + String get labelEditList; 2763 + 2764 + /// Edit profile screen title or action label 2765 + /// 2766 + /// In en, this message translates to: 2767 + /// **'Edit profile'** 2768 + String get labelEditProfile; 2769 + 2770 + /// Edit starter pack dialog title 2771 + /// 2772 + /// In en, this message translates to: 2773 + /// **'Edit starter pack'** 2774 + String get labelEditStarterPack; 2775 + 2776 + /// Feed label 2777 + /// 2778 + /// In en, this message translates to: 2779 + /// **'Feed'** 2780 + String get labelFeed; 2781 + 2782 + /// Followers label 2783 + /// 2784 + /// In en, this message translates to: 2785 + /// **'Followers'** 2786 + String get labelFollowers; 2787 + 2788 + /// Starter pack statistic label for joins this week 2789 + /// 2790 + /// In en, this message translates to: 2791 + /// **'joined this week'** 2792 + String get labelJoinedThisWeek; 2793 + 2794 + /// Starter pack card statistic label for total joins 2795 + /// 2796 + /// In en, this message translates to: 2797 + /// **'joined total'** 2798 + String get labelJoinedTotal; 2799 + 2800 + /// Following label 2801 + /// 2802 + /// In en, this message translates to: 2803 + /// **'Following'** 2804 + String get labelFollowing; 2805 + 2806 + /// Generic list title 2807 + /// 2808 + /// In en, this message translates to: 2809 + /// **'List'** 2810 + String get labelList; 2811 + 2812 + /// Lists label 2813 + /// 2814 + /// In en, this message translates to: 2815 + /// **'Lists'** 2816 + String get labelLists; 2817 + 2818 + /// Profile media tab label 2819 + /// 2820 + /// In en, this message translates to: 2821 + /// **'Media'** 2822 + String get labelMedia; 2823 + 2824 + /// Members section label 2825 + /// 2826 + /// In en, this message translates to: 2827 + /// **'Members'** 2828 + String get labelMembers; 2829 + 2830 + /// Profile context moderation list section heading 2831 + /// 2832 + /// In en, this message translates to: 2833 + /// **'Moderation Lists'** 2834 + String get labelModerationLists; 2835 + 2836 + /// Short moderation list badge label 2837 + /// 2838 + /// In en, this message translates to: 2839 + /// **'MOD'** 2840 + String get labelModerationShort; 2841 + 2842 + /// List action label to mute a list 2843 + /// 2844 + /// In en, this message translates to: 2845 + /// **'Mute list'** 2846 + String get labelMuteList; 2847 + 2848 + /// Mutual follows label 2849 + /// 2850 + /// In en, this message translates to: 2851 + /// **'Mutuals'** 2852 + String get labelMutuals; 2853 + 2854 + /// My lists screen title 2855 + /// 2856 + /// In en, this message translates to: 2857 + /// **'My Lists'** 2858 + String get labelMyLists; 2859 + 2860 + /// Name field label 2861 + /// 2862 + /// In en, this message translates to: 2863 + /// **'Name'** 2864 + String get labelName; 2865 + 2866 + /// New starter pack screen title 2867 + /// 2868 + /// In en, this message translates to: 2869 + /// **'New Starter Pack'** 2870 + String get labelNewStarterPack; 2871 + 2872 + /// Profile context other list section heading 2873 + /// 2874 + /// In en, this message translates to: 2875 + /// **'Other Lists'** 2876 + String get labelOtherLists; 2877 + 2878 + /// Pronouns field label 2879 + /// 2880 + /// In en, this message translates to: 2881 + /// **'Pronouns'** 2882 + String get labelPronouns; 2883 + 2884 + /// Profile context screen title and action label 2885 + /// 2886 + /// In en, this message translates to: 2887 + /// **'Profile Context'** 2888 + String get labelProfileContext; 2889 + 2890 + /// Generic profile screen title 2891 + /// 2892 + /// In en, this message translates to: 2893 + /// **'Profile'** 2894 + String get labelProfileTitle; 2895 + 2896 + /// Starter pack feeds section title 2897 + /// 2898 + /// In en, this message translates to: 2899 + /// **'Recommended Feeds'** 2900 + String get labelRecommendedFeeds; 2901 + 2902 + /// Profile context reference list section heading 2903 + /// 2904 + /// In en, this message translates to: 2905 + /// **'Reference Lists'** 2906 + String get labelReferenceLists; 2907 + 2908 + /// Short reference list badge label 2909 + /// 2910 + /// In en, this message translates to: 2911 + /// **'REFERENCE'** 2912 + String get labelReferenceShort; 2913 + 2914 + /// Profile replies tab label 2915 + /// 2916 + /// In en, this message translates to: 2917 + /// **'Replies'** 2918 + String get labelReplies; 2919 + 2920 + /// Report action label 2921 + /// 2922 + /// In en, this message translates to: 2923 + /// **'Report'** 2924 + String get labelReport; 2925 + 2926 + /// Report account dialog title 2927 + /// 2928 + /// In en, this message translates to: 2929 + /// **'Report Account'** 2930 + String get labelReportAccount; 2931 + 2932 + /// Report reason section label 2933 + /// 2934 + /// In en, this message translates to: 2935 + /// **'Reason'** 2936 + String get labelReportReason; 2937 + 2938 + /// Report explanation field label 2939 + /// 2940 + /// In en, this message translates to: 2941 + /// **'Explanation (required)'** 2942 + String get labelReportReasonExplanationRequired; 2943 + 2944 + /// Report reason label for harassment 2945 + /// 2946 + /// In en, this message translates to: 2947 + /// **'Harassment'** 2948 + String get labelReportReasonHarassment; 2949 + 2950 + /// Report reason label for misleading content 2951 + /// 2952 + /// In en, this message translates to: 2953 + /// **'Misleading'** 2954 + String get labelReportReasonMisleading; 2955 + 2956 + /// Report reason label for other 2957 + /// 2958 + /// In en, this message translates to: 2959 + /// **'Other'** 2960 + String get labelReportReasonOther; 2961 + 2962 + /// Report reason label for sexual content 2963 + /// 2964 + /// In en, this message translates to: 2965 + /// **'Sexual Content'** 2966 + String get labelReportReasonSexualContent; 2967 + 2968 + /// Report reason label for spam 2969 + /// 2970 + /// In en, this message translates to: 2971 + /// **'Spam'** 2972 + String get labelReportReasonSpam; 2973 + 2974 + /// Report reason label for violations 2975 + /// 2976 + /// In en, this message translates to: 2977 + /// **'Violation'** 2978 + String get labelReportReasonViolation; 2979 + 2980 + /// Report success dialog title 2981 + /// 2982 + /// In en, this message translates to: 2983 + /// **'Report Submitted'** 2984 + String get labelReportSubmitted; 2985 + 2986 + /// Select all checkbox label 2987 + /// 2988 + /// In en, this message translates to: 2989 + /// **'Select All'** 2990 + String get labelSelectAll; 2991 + 2992 + /// Feed picker sheet title 2993 + /// 2994 + /// In en, this message translates to: 2995 + /// **'Select a feed'** 2996 + String get labelSelectFeed; 2997 + 2998 + /// Profile action label to share a profile 2999 + /// 3000 + /// In en, this message translates to: 3001 + /// **'Share Profile'** 3002 + String get labelShareProfile; 3003 + 3004 + /// Fallback starter pack title 3005 + /// 3006 + /// In en, this message translates to: 3007 + /// **'Starter Pack'** 3008 + String get labelStarterPack; 3009 + 3010 + /// Type field label 3011 + /// 3012 + /// In en, this message translates to: 3013 + /// **'Type'** 3014 + String get labelType; 3015 + 3016 + /// Starter pack detail statistic label for total joins 3017 + /// 3018 + /// In en, this message translates to: 3019 + /// **'total joined'** 3020 + String get labelTotalJoined; 3021 + 3022 + /// Profile liked posts unavailable entry title 3023 + /// 3024 + /// In en, this message translates to: 3025 + /// **'Unavailable liked post'** 3026 + String get labelUnavailableLikedPost; 3027 + 3028 + /// List action label to unblock accounts via a moderation list 3029 + /// 3030 + /// In en, this message translates to: 3031 + /// **'Unblock via list'** 3032 + String get labelUnblockViaList; 3033 + 3034 + /// List action label to unmute a list 3035 + /// 3036 + /// In en, this message translates to: 3037 + /// **'Unmute list'** 3038 + String get labelUnmuteList; 3039 + 3040 + /// Helper label for selecting up to three starter pack feeds 3041 + /// 3042 + /// In en, this message translates to: 3043 + /// **'(up to 3)'** 3044 + String get labelUpToThree; 3045 + 3046 + /// Website field label 3047 + /// 3048 + /// In en, this message translates to: 3049 + /// **'Website'** 3050 + String get labelWebsite; 3051 + 3052 + /// Profile connection chip for the current user 3053 + /// 3054 + /// In en, this message translates to: 3055 + /// **'You'** 3056 + String get labelYou; 3057 + 3058 + /// Profile context blocked-by explanatory copy 3059 + /// 3060 + /// In en, this message translates to: 3061 + /// **'Blocks are a normal part of social media. This data is public on the AT Protocol.'** 3062 + String get messageBlockedByContextNotice; 3063 + 3064 + /// Profile context message when blocking list is not available 3065 + /// 3066 + /// In en, this message translates to: 3067 + /// **'Blocking information is only available when viewing your own profile.'** 3068 + String get messageBlockingOnlyOwnProfile; 3069 + 3070 + /// Tooltip and semantics label for changing profile avatar 3071 + /// 3072 + /// In en, this message translates to: 3073 + /// **'Change avatar image'** 3074 + String get messageChangeAvatarImage; 3075 + 3076 + /// Tooltip and semantics label for changing profile banner 3077 + /// 3078 + /// In en, this message translates to: 3079 + /// **'Change banner image'** 3080 + String get messageChangeBannerImage; 3081 + 3082 + /// Profile edit validation error for invalid website 3083 + /// 3084 + /// In en, this message translates to: 3085 + /// **'Enter a valid website'** 3086 + String get messageEnterValidWebsite; 3087 + 3088 + /// List detail message when moderation lists do not have feeds 3089 + /// 3090 + /// In en, this message translates to: 3091 + /// **'Feed not available for moderation lists'** 3092 + String get messageFeedUnavailableForModerationLists; 3093 + 3094 + /// Follow audit intro message before scanning 3095 + /// 3096 + /// In en, this message translates to: 3097 + /// **'Scan your follows for deleted, suspended, blocked, and hidden accounts.'** 3098 + String get messageFollowAuditIntro; 3099 + 3100 + /// Follow audit empty prompt before scanning 3101 + /// 3102 + /// In en, this message translates to: 3103 + /// **'Tap Scan to audit your follow list.'** 3104 + String get messageFollowAuditStartPrompt; 3105 + 3106 + /// Profile context empty blocked-by message 3107 + /// 3108 + /// In en, this message translates to: 3109 + /// **'No accounts have blocked this user'** 3110 + String get messageNoAccountsBlockedThisUser; 3111 + 3112 + /// Empty state when no lists exist 3113 + /// 3114 + /// In en, this message translates to: 3115 + /// **'No lists yet'** 3116 + String get messageNoListsYet; 3117 + 3118 + /// Empty starter pack members message 3119 + /// 3120 + /// In en, this message translates to: 3121 + /// **'No members'** 3122 + String get messageNoMembers; 3123 + 3124 + /// Empty list members message 3125 + /// 3126 + /// In en, this message translates to: 3127 + /// **'No members yet'** 3128 + String get messageNoMembersYet; 3129 + 3130 + /// Empty list members message with search instruction 3131 + /// 3132 + /// In en, this message translates to: 3133 + /// **'No members yet. Search above to add people.'** 3134 + String get messageNoMembersYetSearch; 3135 + 3136 + /// Empty profile media tab message 3137 + /// 3138 + /// In en, this message translates to: 3139 + /// **'No media posts yet'** 3140 + String get messageNoMediaPostsYet; 3141 + 3142 + /// Empty profile/list feed message 3143 + /// 3144 + /// In en, this message translates to: 3145 + /// **'No posts yet'** 3146 + String get messageNoPostsYet; 3147 + 3148 + /// Follow audit empty results message 3149 + /// 3150 + /// In en, this message translates to: 3151 + /// **'No problematic follows found'** 3152 + String get messageNoProblematicFollows; 3153 + 3154 + /// Empty profile replies message 3155 + /// 3156 + /// In en, this message translates to: 3157 + /// **'No replies yet'** 3158 + String get messageNoRepliesYet; 3159 + 3160 + /// Follow audit empty filtered results message 3161 + /// 3162 + /// In en, this message translates to: 3163 + /// **'No results visible for the current filters.'** 3164 + String get messageNoResultsForFilters; 3165 + 3166 + /// Empty starter packs message 3167 + /// 3168 + /// In en, this message translates to: 3169 + /// **'No starter packs yet'** 3170 + String get messageNoStarterPacksYet; 3171 + 3172 + /// Empty suggested follows message 3173 + /// 3174 + /// In en, this message translates to: 3175 + /// **'No suggestions found'** 3176 + String get messageNoSuggestionsFound; 3177 + 3178 + /// Profile context empty blocking message 3179 + /// 3180 + /// In en, this message translates to: 3181 + /// **'Not blocking anyone'** 3182 + String get messageNotBlockingAnyone; 3183 + 3184 + /// Profile context empty lists-on message 3185 + /// 3186 + /// In en, this message translates to: 3187 + /// **'Not on any lists'** 3188 + String get messageNotOnAnyLists; 3189 + 3190 + /// Fallback unavailable profile reason 3191 + /// 3192 + /// In en, this message translates to: 3193 + /// **'Profile unavailable'** 3194 + String get messageProfileUnavailable; 3195 + 3196 + /// Snackbar after profile update succeeds 3197 + /// 3198 + /// In en, this message translates to: 3199 + /// **'Profile updated'** 3200 + String get messageProfileUpdated; 3201 + 3202 + /// Report explanation text field hint 3203 + /// 3204 + /// In en, this message translates to: 3205 + /// **'Please explain why you are reporting this...'** 3206 + String get messageReportExplanationHint; 3207 + 3208 + /// Report reason description for harassment 3209 + /// 3210 + /// In en, this message translates to: 3211 + /// **'Harassment or rude behaviour'** 3212 + String get messageReportReasonHarassmentDescription; 3213 + 3214 + /// Report reason description for misleading content 3215 + /// 3216 + /// In en, this message translates to: 3217 + /// **'Misleading or deceptive content'** 3218 + String get messageReportReasonMisleadingDescription; 3219 + 3220 + /// Report reason description for other 3221 + /// 3222 + /// In en, this message translates to: 3223 + /// **'Other reason (requires explanation)'** 3224 + String get messageReportReasonOtherDescription; 3225 + 3226 + /// Report reason description for sexual content 3227 + /// 3228 + /// In en, this message translates to: 3229 + /// **'Unwanted sexual content'** 3230 + String get messageReportReasonSexualContentDescription; 3231 + 3232 + /// Report reason description for spam 3233 + /// 3234 + /// In en, this message translates to: 3235 + /// **'Spam or unsolicited content'** 3236 + String get messageReportReasonSpamDescription; 3237 + 3238 + /// Report reason description for violations 3239 + /// 3240 + /// In en, this message translates to: 3241 + /// **'Violates community guidelines'** 3242 + String get messageReportReasonViolationDescription; 3243 + 3244 + /// Profile connections search field placeholder 3245 + /// 3246 + /// In en, this message translates to: 3247 + /// **'Search handle, name, or description'** 3248 + String get messageSearchConnectionsPlaceholder; 3249 + 3250 + /// Search field placeholder when adding people 3251 + /// 3252 + /// In en, this message translates to: 3253 + /// **'Search for people to add'** 3254 + String get messageSearchPeopleToAddPlaceholder; 3255 + 3256 + /// Profile context message when some blocked accounts are unavailable 3257 + /// 3258 + /// In en, this message translates to: 3259 + /// **'Some blocked accounts are suspended or unavailable.'** 3260 + String get messageSomeBlockedAccountsUnavailable; 3261 + 3262 + /// Suggested follows unavailable message 3263 + /// 3264 + /// In en, this message translates to: 3265 + /// **'Suggested follows are unavailable right now.'** 3266 + String get messageSuggestedFollowsUnavailable; 3267 + 3268 + /// Unavailable accounts card subtitle 3269 + /// 3270 + /// In en, this message translates to: 3271 + /// **'These accounts are suspended or their public profile could not be fetched.'** 3272 + String get messageUnavailableAccountsDescription; 3273 + 3274 + /// Follow audit status label 3275 + /// 3276 + /// In en, this message translates to: 3277 + /// **'Blocked by'** 3278 + String get statusBlockedBy; 3279 + 3280 + /// Follow audit status label 3281 + /// 3282 + /// In en, this message translates to: 3283 + /// **'Blocking'** 3284 + String get statusBlocking; 3285 + 3286 + /// Follow audit status label 3287 + /// 3288 + /// In en, this message translates to: 3289 + /// **'Deactivated'** 3290 + String get statusDeactivated; 3291 + 3292 + /// Follow audit status label 3293 + /// 3294 + /// In en, this message translates to: 3295 + /// **'Deleted'** 3296 + String get statusDeleted; 3297 + 3298 + /// Follow audit status label 3299 + /// 3300 + /// In en, this message translates to: 3301 + /// **'Hidden'** 3302 + String get statusHidden; 3303 + 3304 + /// Follow audit status label 3305 + /// 3306 + /// In en, this message translates to: 3307 + /// **'Mutual block'** 3308 + String get statusMutualBlock; 3309 + 3310 + /// Follow audit status label 3311 + /// 3312 + /// In en, this message translates to: 3313 + /// **'Self-follow'** 3314 + String get statusSelfFollow; 3315 + 3316 + /// Follow audit status label 3317 + /// 3318 + /// In en, this message translates to: 3319 + /// **'Suspended'** 3320 + String get statusSuspended; 3321 + 3322 + /// Tooltip for clearing a search field 3323 + /// 3324 + /// In en, this message translates to: 3325 + /// **'Clear search'** 3326 + String get tooltipClearSearch; 3327 + 3328 + /// Tooltip for jump to top button 3329 + /// 3330 + /// In en, this message translates to: 3331 + /// **'Jump to top'** 3332 + String get tooltipJumpToTop; 3333 + 3334 + /// Profile edit validation error for invalid website 3335 + /// 3336 + /// In en, this message translates to: 3337 + /// **'Enter a valid website'** 3338 + String get validationEnterValidWebsite; 3339 + 3340 + /// Empty state message in the account switcher sheet 3341 + /// 3342 + /// In en, this message translates to: 3343 + /// **'No other signed-in accounts yet. Add an account to switch between profiles.'** 3344 + String get accountSwitcherNoOtherAccounts; 3345 + 3346 + /// Button and dialog title to add another account 3347 + /// 3348 + /// In en, this message translates to: 3349 + /// **'Add Account'** 3350 + String get buttonAddAccount; 3351 + 3352 + /// Message thread menu item to copy all messages 3353 + /// 3354 + /// In en, this message translates to: 3355 + /// **'Copy All'** 3356 + String get buttonCopyAll; 3357 + 3358 + /// Button label to mark notifications read 3359 + /// 3360 + /// In en, this message translates to: 3361 + /// **'Mark All Read'** 3362 + String get buttonMarkAllRead; 3363 + 3364 + /// Snackbar message when adding an account fails 3365 + /// 3366 + /// In en, this message translates to: 3367 + /// **'Failed to add account'** 3368 + String get errorFailedToAddAccount; 3369 + 3370 + /// Error title when messages cannot load 3371 + /// 3372 + /// In en, this message translates to: 3373 + /// **'Failed to load messages'** 3374 + String get errorFailedToLoadMessages; 3375 + 3376 + /// Error title when notifications cannot load 3377 + /// 3378 + /// In en, this message translates to: 3379 + /// **'Failed to load notifications'** 3380 + String get errorFailedToLoadNotifications; 3381 + 3382 + /// Snackbar message when removing an account from account switcher fails 3383 + /// 3384 + /// In en, this message translates to: 3385 + /// **'Unable to remove account right now.'** 3386 + String get errorUnableToRemoveAccountNow; 3387 + 3388 + /// Two-person actor summary in grouped notifications 3389 + /// 3390 + /// In en, this message translates to: 3391 + /// **'{first} and {second}'** 3392 + String formatActorListTwo(String first, String second); 3393 + 3394 + /// Multi-person actor summary in grouped notifications 3395 + /// 3396 + /// In en, this message translates to: 3397 + /// **'{first}, {second}, and {count} others'** 3398 + String formatActorListWithOthers(String first, String second, int count); 3399 + 3400 + /// Month and day label for notification sections 3401 + /// 3402 + /// In en, this message translates to: 3403 + /// **'{month} {day}'** 3404 + String formatMonthDay(String month, int day); 3405 + 3406 + /// Confirmation dialog body when removing an account from the account switcher 3407 + /// 3408 + /// In en, this message translates to: 3409 + /// **'Remove @{handle} from this device?'** 3410 + String formatRemoveAccountContent(String handle); 3411 + 3412 + /// Account switcher sheet title 3413 + /// 3414 + /// In en, this message translates to: 3415 + /// **'Accounts'** 3416 + String get labelAccounts; 3417 + 3418 + /// Alerts screen title 3419 + /// 3420 + /// In en, this message translates to: 3421 + /// **'Alerts'** 3422 + String get labelAlertsTitle; 3423 + 3424 + /// Fallback conversation title 3425 + /// 3426 + /// In en, this message translates to: 3427 + /// **'Conversation'** 3428 + String get labelConversation; 3429 + 3430 + /// Notification channel name for follows 3431 + /// 3432 + /// In en, this message translates to: 3433 + /// **'Follows'** 3434 + String get labelFollows; 3435 + 3436 + /// Notification channel name for likes 3437 + /// 3438 + /// In en, this message translates to: 3439 + /// **'Likes'** 3440 + String get labelLikes; 3441 + 3442 + /// Message requests tab label 3443 + /// 3444 + /// In en, this message translates to: 3445 + /// **'Requests'** 3446 + String get labelMessageRequests; 3447 + 3448 + /// Other notification channel name 3449 + /// 3450 + /// In en, this message translates to: 3451 + /// **'Other'** 3452 + String get labelOther; 3453 + 3454 + /// Primary messages tab label 3455 + /// 3456 + /// In en, this message translates to: 3457 + /// **'Primary'** 3458 + String get labelPrimary; 3459 + 3460 + /// Fallback actor summary for grouped notifications 3461 + /// 3462 + /// In en, this message translates to: 3463 + /// **'Someone'** 3464 + String get labelSomeone; 3465 + 3466 + /// Snackbar message after copying a message 3467 + /// 3468 + /// In en, this message translates to: 3469 + /// **'Message copied'** 3470 + String get messageCopied; 3471 + 3472 + /// Deleted message placeholder 3473 + /// 3474 + /// In en, this message translates to: 3475 + /// **'Message deleted'** 3476 + String get messageDeleted; 3477 + 3478 + /// Local notification body fallback for unknown notification reasons 3479 + /// 3480 + /// In en, this message translates to: 3481 + /// **'sent a notification'** 3482 + String get messageLocalNotificationFallbackBody; 3483 + 3484 + /// Local notification title fallback 3485 + /// 3486 + /// In en, this message translates to: 3487 + /// **'New notification'** 3488 + String get messageNewNotification; 3489 + 3490 + /// Offline state title 3491 + /// 3492 + /// In en, this message translates to: 3493 + /// **'No connection'** 3494 + String get messageNoConnection; 3495 + 3496 + /// Empty primary conversations state 3497 + /// 3498 + /// In en, this message translates to: 3499 + /// **'No conversations yet'** 3500 + String get messageNoConversationsYet; 3501 + 3502 + /// Empty message requests state 3503 + /// 3504 + /// In en, this message translates to: 3505 + /// **'No message requests'** 3506 + String get messageNoMessageRequests; 3507 + 3508 + /// Empty message thread state 3509 + /// 3510 + /// In en, this message translates to: 3511 + /// **'No messages yet'** 3512 + String get messageNoMessagesYet; 3513 + 3514 + /// Empty notifications state 3515 + /// 3516 + /// In en, this message translates to: 3517 + /// **'No notifications yet'** 3518 + String get messageNoNotificationsYet; 3519 + 3520 + /// Notification summary for contact match 3521 + /// 3522 + /// In en, this message translates to: 3523 + /// **'joined from your contacts'** 3524 + String get messageNotificationContactMatch; 3525 + 3526 + /// Notification summary for follows 3527 + /// 3528 + /// In en, this message translates to: 3529 + /// **'followed you'** 3530 + String get messageNotificationFollow; 3531 + 3532 + /// Notification summary fallback 3533 + /// 3534 + /// In en, this message translates to: 3535 + /// **'interacted with you'** 3536 + String get messageNotificationInteracted; 3537 + 3538 + /// Notification summary for likes 3539 + /// 3540 + /// In en, this message translates to: 3541 + /// **'liked your post'** 3542 + String get messageNotificationLike; 3543 + 3544 + /// Notification summary for likes via repost 3545 + /// 3546 + /// In en, this message translates to: 3547 + /// **'liked your repost'** 3548 + String get messageNotificationLikeViaRepost; 3549 + 3550 + /// Notification summary for mentions 3551 + /// 3552 + /// In en, this message translates to: 3553 + /// **'mentioned you'** 3554 + String get messageNotificationMention; 3555 + 3556 + /// Notification summary for quotes 3557 + /// 3558 + /// In en, this message translates to: 3559 + /// **'quoted your post'** 3560 + String get messageNotificationQuote; 3561 + 3562 + /// Notification summary for replies 3563 + /// 3564 + /// In en, this message translates to: 3565 + /// **'replied to your post'** 3566 + String get messageNotificationReply; 3567 + 3568 + /// Notification summary for reposts 3569 + /// 3570 + /// In en, this message translates to: 3571 + /// **'reposted your post'** 3572 + String get messageNotificationRepost; 3573 + 3574 + /// Notification summary for reposts via repost 3575 + /// 3576 + /// In en, this message translates to: 3577 + /// **'reposted your repost'** 3578 + String get messageNotificationRepostViaRepost; 3579 + 3580 + /// Notification summary for starter pack joins 3581 + /// 3582 + /// In en, this message translates to: 3583 + /// **'joined via your starter pack'** 3584 + String get messageNotificationStarterPackJoined; 3585 + 3586 + /// Notification summary for subscribed posts 3587 + /// 3588 + /// In en, this message translates to: 3589 + /// **'posted a new update'** 3590 + String get messageNotificationSubscribedPost; 3591 + 3592 + /// Notification summary for unverified account 3593 + /// 3594 + /// In en, this message translates to: 3595 + /// **'removed your verification'** 3596 + String get messageNotificationUnverified; 3597 + 3598 + /// Notification summary for verified account 3599 + /// 3600 + /// In en, this message translates to: 3601 + /// **'verified your account'** 3602 + String get messageNotificationVerified; 3603 + 3604 + /// Message composer placeholder 3605 + /// 3606 + /// In en, this message translates to: 3607 + /// **'Message…'** 3608 + String get messagePlaceholder; 3609 + 3610 + /// Snackbar when account switch requires reauthentication 3611 + /// 3612 + /// In en, this message translates to: 3613 + /// **'Please sign in again for that account.'** 3614 + String get messagePleaseSignInAgainForAccount; 3615 + 3616 + /// Offline message list explanation 3617 + /// 3618 + /// In en, this message translates to: 3619 + /// **'Reconnect to load messages.'** 3620 + String get messageReconnectToLoadMessages; 3621 + 3622 + /// Offline notifications explanation 3623 + /// 3624 + /// In en, this message translates to: 3625 + /// **'Reconnect to load notifications.'** 3626 + String get messageReconnectToLoadNotifications; 3627 + 3628 + /// Snackbar after copying a message thread 3629 + /// 3630 + /// In en, this message translates to: 3631 + /// **'Thread copied'** 3632 + String get messageThreadCopied; 3633 + 3634 + /// Today date section label 3635 + /// 3636 + /// In en, this message translates to: 3637 + /// **'Today'** 3638 + String get messageToday; 3639 + 3640 + /// Yesterday date section label 3641 + /// 3642 + /// In en, this message translates to: 3643 + /// **'Yesterday'** 3644 + String get messageYesterday; 3645 + 3646 + /// Handle placeholder in account switcher 3647 + /// 3648 + /// In en, this message translates to: 3649 + /// **'username.bsky.social'** 3650 + String get placeholderUsernameBskySocial; 3651 + 3652 + /// Account switcher validation error for empty handle or DID 3653 + /// 3654 + /// In en, this message translates to: 3655 + /// **'Enter a Bluesky handle or DID'** 3656 + String get validationEnterBlueskyHandleOrDid; 3657 + 3658 + /// Account switcher validation error for incomplete DID 3659 + /// 3660 + /// In en, this message translates to: 3661 + /// **'Enter a complete DID like did:plc:... or did:web:...'** 3662 + String get validationEnterCompleteDid; 3663 + 3664 + /// Account switcher validation error for invalid handle 3665 + /// 3666 + /// In en, this message translates to: 3667 + /// **'Enter a full handle like username.bsky.social'** 3668 + String get validationEnterFullHandle; 3669 + 3670 + /// Account switcher validation error for unsupported DID method 3671 + /// 3672 + /// In en, this message translates to: 3673 + /// **'Use a did:plc:... or did:web:... identifier'** 3674 + String get validationUseSupportedDid; 3675 + 1180 3676 /// Validation error for missing app password 1181 3677 /// 1182 3678 /// In en, this message translates to: 1183 3679 /// **'Enter your app password'** 1184 3680 String get validationEnterAppPassword; 3681 + 3682 + /// Generic add button label 3683 + /// 3684 + /// In en, this message translates to: 3685 + /// **'Add'** 3686 + String get buttonAdd; 3687 + 3688 + /// Button label while an add action is in progress 3689 + /// 3690 + /// In en, this message translates to: 3691 + /// **'Adding...'** 3692 + String get buttonAdding; 3693 + 3694 + /// Devtools button label linking to pds.ls 3695 + /// 3696 + /// In en, this message translates to: 3697 + /// **'Inspired by pds.ls'** 3698 + String get buttonInspiredByPdsLs; 3699 + 3700 + /// Button label to copy record JSON 3701 + /// 3702 + /// In en, this message translates to: 3703 + /// **'Copy JSON'** 3704 + String get buttonCopyJson; 3705 + 3706 + /// Button label to resolve a handle, DID, or AT URI 3707 + /// 3708 + /// In en, this message translates to: 3709 + /// **'Resolve'** 3710 + String get buttonResolve; 3711 + 3712 + /// Button label to unsubscribe from a labeler 3713 + /// 3714 + /// In en, this message translates to: 3715 + /// **'Unsubscribe'** 3716 + String get buttonUnsubscribe; 3717 + 3718 + /// Dialog title for adding a moderation labeler 3719 + /// 3720 + /// In en, this message translates to: 3721 + /// **'Add labeler'** 3722 + String get dialogAddLabelerTitle; 3723 + 3724 + /// Confirmation dialog body before clearing all log files 3725 + /// 3726 + /// In en, this message translates to: 3727 + /// **'This will permanently delete all log files. This action cannot be undone.'** 3728 + String get dialogClearAllLogsContent; 3729 + 3730 + /// Confirmation dialog title before clearing all log files 3731 + /// 3732 + /// In en, this message translates to: 3733 + /// **'Clear all logs?'** 3734 + String get dialogClearAllLogsTitle; 3735 + 3736 + /// Error title when logs cannot load 3737 + /// 3738 + /// In en, this message translates to: 3739 + /// **'Failed to load logs'** 3740 + String get errorFailedToLoadLogs; 3741 + 3742 + /// Error title when moderation settings cannot load 3743 + /// 3744 + /// In en, this message translates to: 3745 + /// **'Failed to load moderation settings'** 3746 + String get errorFailedToLoadModerationSettings; 3747 + 3748 + /// Snackbar message when unsubscribing from a labeler fails 3749 + /// 3750 + /// In en, this message translates to: 3751 + /// **'Failed to unsubscribe: {error}'** 3752 + String errorFailedToUnsubscribeLabeler(Object error); 3753 + 3754 + /// Snackbar message when adult content preference update fails 3755 + /// 3756 + /// In en, this message translates to: 3757 + /// **'Failed to update adult content: {error}'** 3758 + String errorFailedToUpdateAdultContent(Object error); 3759 + 3760 + /// Snackbar message when a label preference update fails 3761 + /// 3762 + /// In en, this message translates to: 3763 + /// **'Failed to update preference: {error}'** 3764 + String errorFailedToUpdateLabelPreference(Object error); 3765 + 3766 + /// Snackbar message when a labeler subscription update fails 3767 + /// 3768 + /// In en, this message translates to: 3769 + /// **'Failed to update subscription: {error}'** 3770 + String errorFailedToUpdateLabelerSubscription(Object error); 3771 + 3772 + /// Validation error when adding a labeler without a DID 3773 + /// 3774 + /// In en, this message translates to: 3775 + /// **'Enter a labeler DID.'** 3776 + String get errorLabelerDidRequired; 3777 + 3778 + /// Error thrown when a labeler DID cannot be loaded 3779 + /// 3780 + /// In en, this message translates to: 3781 + /// **'Labeler not found.'** 3782 + String get errorLabelerNotFound; 3783 + 3784 + /// Validation error when no labeler exists for the entered DID 3785 + /// 3786 + /// In en, this message translates to: 3787 + /// **'No labeler found for that DID.'** 3788 + String get errorNoLabelerFoundForDid; 3789 + 3790 + /// Error title when a labeler detail screen cannot load 3791 + /// 3792 + /// In en, this message translates to: 3793 + /// **'Unable to load labeler'** 3794 + String get errorUnableToLoadLabeler; 3795 + 3796 + /// Add labeler button showing current and maximum custom labelers 3797 + /// 3798 + /// In en, this message translates to: 3799 + /// **'Add ({current}/{max})'** 3800 + String formatAddLabelerLimit(int current, int max); 3801 + 3802 + /// Devtools repository collection count 3803 + /// 3804 + /// In en, this message translates to: 3805 + /// **'{count, plural, =1{1 collection} other{{count} collections}}'** 3806 + String formatCollectionsCount(int count); 3807 + 3808 + /// Devtools record CID label 3809 + /// 3810 + /// In en, this message translates to: 3811 + /// **'CID: {cid}'** 3812 + String formatCid(String cid); 3813 + 3814 + /// Moderation labeler detail chip showing custom label count 3815 + /// 3816 + /// In en, this message translates to: 3817 + /// **'{count, plural, =1{1 custom label} other{{count} custom labels}}'** 3818 + String formatCustomLabelCount(int count); 3819 + 3820 + /// Moderation labeler card chip showing custom definition count 3821 + /// 3822 + /// In en, this message translates to: 3823 + /// **'{count, plural, =1{1 definition} other{{count} definitions}}'** 3824 + String formatDefinitionCount(int count); 3825 + 3826 + /// Devtools record list count while total is unknown 3827 + /// 3828 + /// In en, this message translates to: 3829 + /// **'{count} loaded'** 3830 + String formatLoadedRecordsCount(int count); 3831 + 3832 + /// Devtools record list count showing loaded and total records 3833 + /// 3834 + /// In en, this message translates to: 3835 + /// **'{loaded} of {total}'** 3836 + String formatLoadedRecordsOfTotal(int loaded, int total); 3837 + 3838 + /// Tooltip description for a moderation label source 3839 + /// 3840 + /// In en, this message translates to: 3841 + /// **'{source} label'** 3842 + String formatModerationSourceLabel(String source); 3843 + 3844 + /// Moderation policy chip showing the blur behavior 3845 + /// 3846 + /// In en, this message translates to: 3847 + /// **'Blur {value}'** 3848 + String formatPolicyBlur(String value); 3849 + 3850 + /// Moderation policy chip showing default preference 3851 + /// 3852 + /// In en, this message translates to: 3853 + /// **'Default {value}'** 3854 + String formatPolicyDefault(String value); 3855 + 3856 + /// Moderation policy chip showing a label identifier 3857 + /// 3858 + /// In en, this message translates to: 3859 + /// **'ID {identifier}'** 3860 + String formatPolicyId(String identifier); 3861 + 3862 + /// Moderation policy chip showing severity 3863 + /// 3864 + /// In en, this message translates to: 3865 + /// **'Severity {value}'** 3866 + String formatPolicySeverity(String value); 3867 + 3868 + /// Moderation labeler card chip showing published value count 3869 + /// 3870 + /// In en, this message translates to: 3871 + /// **'{count, plural, =1{1 published value} other{{count} published values}}'** 3872 + String formatPublishedValueCount(int count); 3873 + 3874 + /// Devtools repository record count 3875 + /// 3876 + /// In en, this message translates to: 3877 + /// **'{count, plural, =1{1 record} other{{count} records}}'** 3878 + String formatRecordsCount(int count); 3879 + 3880 + /// Snackbar after subscribing to a labeler 3881 + /// 3882 + /// In en, this message translates to: 3883 + /// **'Subscribed to {name}'** 3884 + String formatSubscribedToLabeler(String name); 3885 + 3886 + /// Moderation settings adult content switch label 3887 + /// 3888 + /// In en, this message translates to: 3889 + /// **'Adult content'** 3890 + String get labelAdultContentSetting; 3891 + 3892 + /// Short chip label for adult-only moderation labels 3893 + /// 3894 + /// In en, this message translates to: 3895 + /// **'18+'** 3896 + String get labelAdultOnlyShort; 3897 + 3898 + /// Status label for always-on moderation labeler 3899 + /// 3900 + /// In en, this message translates to: 3901 + /// **'Always on'** 3902 + String get labelAlwaysOn; 3903 + 3904 + /// Log viewer auto-scroll control label 3905 + /// 3906 + /// In en, this message translates to: 3907 + /// **'Auto-scroll'** 3908 + String get labelAutoScroll; 3909 + 3910 + /// Built-in Bluesky moderation labeler title 3911 + /// 3912 + /// In en, this message translates to: 3913 + /// **'Bluesky moderation'** 3914 + String get labelBlueskyModeration; 3915 + 3916 + /// Chip label for a built-in item 3917 + /// 3918 + /// In en, this message translates to: 3919 + /// **'Built-in'** 3920 + String get labelBuiltIn; 3921 + 3922 + /// Moderation settings section title for built-in labeler 3923 + /// 3924 + /// In en, this message translates to: 3925 + /// **'Built-in labeler'** 3926 + String get labelBuiltInLabeler; 3927 + 3928 + /// Moderation labeler chip and switch label for built-in moderation 3929 + /// 3930 + /// In en, this message translates to: 3931 + /// **'Built-in moderation'** 3932 + String get labelBuiltInModeration; 3933 + 3934 + /// Moderation badge for blocked account content 3935 + /// 3936 + /// In en, this message translates to: 3937 + /// **'Blocked account'** 3938 + String get labelBlockedAccount; 3939 + 3940 + /// Moderation badge for content from an account that blocked the user 3941 + /// 3942 + /// In en, this message translates to: 3943 + /// **'Blocked by account'** 3944 + String get labelBlockedByAccount; 3945 + 3946 + /// Moderation badge for content limited by a block relationship 3947 + /// 3948 + /// In en, this message translates to: 3949 + /// **'Blocked relationship'** 3950 + String get labelBlockedRelationship; 3951 + 3952 + /// Devtools section heading for repository collections 3953 + /// 3954 + /// In en, this message translates to: 3955 + /// **'COLLECTIONS'** 3956 + String get labelCollections; 3957 + 3958 + /// Moderation label preference option to hide matching content 3959 + /// 3960 + /// In en, this message translates to: 3961 + /// **'Hide'** 3962 + String get labelContentPreferenceHide; 3963 + 3964 + /// Moderation label preference option to ignore matching content 3965 + /// 3966 + /// In en, this message translates to: 3967 + /// **'Ignore'** 3968 + String get labelContentPreferenceIgnore; 3969 + 3970 + /// Moderation label preference option to warn on matching content 3971 + /// 3972 + /// In en, this message translates to: 3973 + /// **'Warn'** 3974 + String get labelContentPreferenceWarn; 3975 + 3976 + /// Moderation settings section title for custom labelers 3977 + /// 3978 + /// In en, this message translates to: 3979 + /// **'Custom labelers'** 3980 + String get labelCustomLabelers; 3981 + 3982 + /// Labeler detail screen app bar title 3983 + /// 3984 + /// In en, this message translates to: 3985 + /// **'Labeler'** 3986 + String get labelLabeler; 3987 + 3988 + /// Input label for labeler DID 3989 + /// 3990 + /// In en, this message translates to: 3991 + /// **'Labeler DID'** 3992 + String get labelLabelerDid; 3993 + 3994 + /// Moderation settings hero title 3995 + /// 3996 + /// In en, this message translates to: 3997 + /// **'Labelers and content moderation'** 3998 + String get labelLabelersAndContentModeration; 3999 + 4000 + /// Labeler detail section heading for preferences 4001 + /// 4002 + /// In en, this message translates to: 4003 + /// **'Label preferences'** 4004 + String get labelLabelPreferences; 4005 + 4006 + /// Moderation badge for hidden content 4007 + /// 4008 + /// In en, this message translates to: 4009 + /// **'Hidden content'** 4010 + String get labelHiddenContent; 4011 + 4012 + /// Log level filter chip for debug logs 4013 + /// 4014 + /// In en, this message translates to: 4015 + /// **'Debug'** 4016 + String get labelLogLevelDebug; 4017 + 4018 + /// Log level filter chip for error logs 4019 + /// 4020 + /// In en, this message translates to: 4021 + /// **'Error'** 4022 + String get labelLogLevelError; 4023 + 4024 + /// Log level filter chip for fatal logs 4025 + /// 4026 + /// In en, this message translates to: 4027 + /// **'Fatal'** 4028 + String get labelLogLevelFatal; 4029 + 4030 + /// Log level filter chip for info logs 4031 + /// 4032 + /// In en, this message translates to: 4033 + /// **'Info'** 4034 + String get labelLogLevelInfo; 4035 + 4036 + /// Log level filter chip for trace logs 4037 + /// 4038 + /// In en, this message translates to: 4039 + /// **'Trace'** 4040 + String get labelLogLevelTrace; 4041 + 4042 + /// Log level filter chip for warning logs 4043 + /// 4044 + /// In en, this message translates to: 4045 + /// **'Warning'** 4046 + String get labelLogLevelWarning; 4047 + 4048 + /// Fallback informational moderation badge label 4049 + /// 4050 + /// In en, this message translates to: 4051 + /// **'Moderation note'** 4052 + String get labelModerationNote; 4053 + 4054 + /// Moderation source label for the built-in Bluesky labeler 4055 + /// 4056 + /// In en, this message translates to: 4057 + /// **'Bluesky'** 4058 + String get labelModerationSourceBluesky; 4059 + 4060 + /// Moderation source label for a subscribed custom labeler 4061 + /// 4062 + /// In en, this message translates to: 4063 + /// **'Subscribed labeler'** 4064 + String get labelModerationSourceSubscribedLabeler; 4065 + 4066 + /// Moderation badge for muted account content 4067 + /// 4068 + /// In en, this message translates to: 4069 + /// **'Muted account'** 4070 + String get labelMutedAccount; 4071 + 4072 + /// Moderation badge for muted phrase content 4073 + /// 4074 + /// In en, this message translates to: 4075 + /// **'Muted phrase'** 4076 + String get labelMutedPhrase; 4077 + 4078 + /// Empty state title when a labeler has no localized custom definitions 4079 + /// 4080 + /// In en, this message translates to: 4081 + /// **'No custom label definitions'** 4082 + String get labelNoCustomLabelDefinitions; 4083 + 4084 + /// Empty state title when no custom labelers are subscribed 4085 + /// 4086 + /// In en, this message translates to: 4087 + /// **'No custom labelers'** 4088 + String get labelNoCustomLabelers; 4089 + 4090 + /// Developer PDS Explorer screen title 4091 + /// 4092 + /// In en, this message translates to: 4093 + /// **'PDS Explorer'** 4094 + String get labelPdsExplorer; 4095 + 4096 + /// Labeler detail section heading for published policies 4097 + /// 4098 + /// In en, this message translates to: 4099 + /// **'Published policies'** 4100 + String get labelPublishedPolicies; 4101 + 4102 + /// Devtools breadcrumb label for an empty record key 4103 + /// 4104 + /// In en, this message translates to: 4105 + /// **'Record JSON'** 4106 + String get labelRecordJson; 4107 + 4108 + /// Generic refresh tooltip label 4109 + /// 4110 + /// In en, this message translates to: 4111 + /// **'Refresh'** 4112 + String get labelRefresh; 4113 + 4114 + /// Devtools breadcrumb fallback label for repository 4115 + /// 4116 + /// In en, this message translates to: 4117 + /// **'Repository'** 4118 + String get labelRepository; 4119 + 4120 + /// Labeler detail switch label for subscribed labelers 4121 + /// 4122 + /// In en, this message translates to: 4123 + /// **'Subscribed'** 4124 + String get labelSubscribed; 4125 + 4126 + /// Fallback moderation label for sensitive content 4127 + /// 4128 + /// In en, this message translates to: 4129 + /// **'Sensitive content'** 4130 + String get labelSensitiveContent; 4131 + 4132 + /// Generic fallback label when a name is unknown 4133 + /// 4134 + /// In en, this message translates to: 4135 + /// **'Unknown'** 4136 + String get labelUnknown; 4137 + 4138 + /// Helper text in add labeler dialog 4139 + /// 4140 + /// In en, this message translates to: 4141 + /// **'Paste a labeler DID to review and subscribe to its labels.'** 4142 + String get messageAddLabelerDidHelper; 4143 + 4144 + /// Moderation settings adult content switch helper text 4145 + /// 4146 + /// In en, this message translates to: 4147 + /// **'Required before 18+ label preferences can be changed.'** 4148 + String get messageAdultContentRequiredForLabels; 4149 + 4150 + /// Moderation settings fallback subtitle when built-in labeler details are unavailable 4151 + /// 4152 + /// In en, this message translates to: 4153 + /// **'The built-in labeler is active even if its details cannot be loaded right now.'** 4154 + String get messageBuiltInLabelerActiveWhenUnavailable; 4155 + 4156 + /// Labeler detail subtitle for built-in labeler subscription switch 4157 + /// 4158 + /// In en, this message translates to: 4159 + /// **'This labeler is always active.'** 4160 + String get messageBuiltInLabelerAlwaysActive; 4161 + 4162 + /// Developer PDS Explorer empty state helper text 4163 + /// 4164 + /// In en, this message translates to: 4165 + /// **'Enter a handle, DID, or AT-URI to explore\na user\'\'s repository.'** 4166 + String get messageDevtoolsEmptyState; 4167 + 4168 + /// Helper text shown when adult content is required to edit a label preference 4169 + /// 4170 + /// In en, this message translates to: 4171 + /// **'Enable adult content to change this 18+ label.'** 4172 + String get messageEnableAdultContentForLabel; 4173 + 4174 + /// Tooltip for blocked account moderation badge 4175 + /// 4176 + /// In en, this message translates to: 4177 + /// **'This account is blocked'** 4178 + String get messageBlockedAccountDescription; 4179 + 4180 + /// Tooltip for blocked-by account moderation badge 4181 + /// 4182 + /// In en, this message translates to: 4183 + /// **'This account has blocked you'** 4184 + String get messageBlockedByAccountDescription; 4185 + 4186 + /// Tooltip for blocked relationship moderation badge 4187 + /// 4188 + /// In en, this message translates to: 4189 + /// **'This content is limited by a block relationship'** 4190 + String get messageBlockedRelationshipDescription; 4191 + 4192 + /// Tooltip for hidden content moderation badge 4193 + /// 4194 + /// In en, this message translates to: 4195 + /// **'This content is hidden by moderation rules'** 4196 + String get messageHiddenContentDescription; 4197 + 4198 + /// Snackbar after copying record JSON 4199 + /// 4200 + /// In en, this message translates to: 4201 + /// **'JSON copied to clipboard'** 4202 + String get messageJsonCopiedToClipboard; 4203 + 4204 + /// Log viewer empty state title 4205 + /// 4206 + /// In en, this message translates to: 4207 + /// **'No logs yet'** 4208 + String get messageLogsEmpty; 4209 + 4210 + /// Log viewer empty state subtitle 4211 + /// 4212 + /// In en, this message translates to: 4213 + /// **'Log entries will appear here'** 4214 + String get messageLogsEmptySubtitle; 4215 + 4216 + /// Moderation settings hero subtitle 4217 + /// 4218 + /// In en, this message translates to: 4219 + /// **'Manage adult-content visibility, subscribed labelers, and the rules each labeler applies to posts and profiles.'** 4220 + String get messageModerationSettingsHeroSubtitle; 4221 + 4222 + /// Fallback moderation badge tooltip 4223 + /// 4224 + /// In en, this message translates to: 4225 + /// **'Moderation guidance applies here'** 4226 + String get messageModerationGuidanceApplies; 4227 + 4228 + /// Tooltip for muted account moderation badge 4229 + /// 4230 + /// In en, this message translates to: 4231 + /// **'Muted content is being downranked here'** 4232 + String get messageMutedAccountDescription; 4233 + 4234 + /// Tooltip for muted phrase moderation badge 4235 + /// 4236 + /// In en, this message translates to: 4237 + /// **'A muted phrase matched this content'** 4238 + String get messageMutedPhraseDescription; 4239 + 4240 + /// Labeler detail empty state subtitle when no localized custom definitions are available 4241 + /// 4242 + /// In en, this message translates to: 4243 + /// **'This labeler publishes values, but not localized custom definitions.'** 4244 + String get messageNoCustomLabelDefinitions; 4245 + 4246 + /// Moderation settings empty state subtitle when no custom labelers are subscribed 4247 + /// 4248 + /// In en, this message translates to: 4249 + /// **'Add a labeler DID to subscribe and configure its custom labels.'** 4250 + String get messageNoCustomLabelers; 4251 + 4252 + /// Fallback description for a label definition without localized description 4253 + /// 4254 + /// In en, this message translates to: 4255 + /// **'No description available for this label.'** 4256 + String get messageNoLabelDescriptionAvailable; 4257 + 4258 + /// Snackbar when there is no log file to share 4259 + /// 4260 + /// In en, this message translates to: 4261 + /// **'No log file available'** 4262 + String get messageNoLogFileAvailable; 4263 + 4264 + /// Devtools repository count status when record counts cannot be loaded 4265 + /// 4266 + /// In en, this message translates to: 4267 + /// **'Record counts unavailable'** 4268 + String get messageRecordCountsUnavailable; 4269 + 4270 + /// Devtools repository count status while record counts are loading 4271 + /// 4272 + /// In en, this message translates to: 4273 + /// **'Counting records...'** 4274 + String get messageRecordCountsLoading; 4275 + 4276 + /// Labeler detail subtitle for custom labeler subscription switch 4277 + /// 4278 + /// In en, this message translates to: 4279 + /// **'Subscribed labelers are added to your moderation headers and preferences.'** 4280 + String get messageSubscribedLabelersHeaders; 4281 + 4282 + /// Snackbar when the log share sheet cannot open 4283 + /// 4284 + /// In en, this message translates to: 4285 + /// **'Unable to open share sheet. Please try again.'** 4286 + String get messageUnableToOpenShareSheet; 4287 + 4288 + /// Devtools search input placeholder 4289 + /// 4290 + /// In en, this message translates to: 4291 + /// **'Handle, DID, or at:// URI'** 4292 + String get placeholderHandleDidOrAtUri; 4293 + 4294 + /// Placeholder DID in add labeler dialog 4295 + /// 4296 + /// In en, this message translates to: 4297 + /// **'did:plc:examplelabeler'** 4298 + String get placeholderLabelerDid; 4299 + 4300 + /// Log viewer search field placeholder 4301 + /// 4302 + /// In en, this message translates to: 4303 + /// **'Filter logs...'** 4304 + String get placeholderLogsFilter; 4305 + 4306 + /// Share sheet subject for log files 4307 + /// 4308 + /// In en, this message translates to: 4309 + /// **'Lazurite logs'** 4310 + String get subjectLazuriteLogs; 4311 + 4312 + /// Tooltip for clearing all log files 4313 + /// 4314 + /// In en, this message translates to: 4315 + /// **'Clear all logs'** 4316 + String get tooltipClearAllLogs; 4317 + 4318 + /// Tooltip for opening pds.ls 4319 + /// 4320 + /// In en, this message translates to: 4321 + /// **'Go to pds.ls'** 4322 + String get tooltipGoToPdsLs; 4323 + 4324 + /// Tooltip for sharing a log file 4325 + /// 4326 + /// In en, this message translates to: 4327 + /// **'Share log file'** 4328 + String get tooltipShareLogFile; 1185 4329 } 1186 4330 1187 4331 class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
+1773
lib/core/l10n/app_localizations_en.dart
··· 18 18 String get buttonCancel => 'Cancel'; 19 19 20 20 @override 21 + String get buttonCompose => 'Compose'; 22 + 23 + @override 21 24 String get buttonClearCache => 'Clear Cache'; 22 25 23 26 @override 24 27 String get buttonContinue => 'Continue'; 25 28 26 29 @override 30 + String get buttonClearLocal => 'Clear Local'; 31 + 32 + @override 33 + String get buttonDelete => 'Delete'; 34 + 35 + @override 36 + String get buttonDiscard => 'Discard'; 37 + 38 + @override 39 + String get buttonLoadMoreQuotes => 'Load more quotes'; 40 + 41 + @override 42 + String get buttonLoadMoreReposts => 'Load more reposts'; 43 + 44 + @override 27 45 String get buttonRemove => 'Remove'; 28 46 29 47 @override ··· 39 57 String get buttonOpen => 'Open'; 40 58 41 59 @override 60 + String get buttonOk => 'OK'; 61 + 62 + @override 63 + String get buttonPost => 'Post'; 64 + 65 + @override 42 66 String get buttonResetSignInData => 'Reset Sign-In Data'; 43 67 44 68 @override 45 69 String get buttonRetry => 'Retry'; 46 70 47 71 @override 72 + String get buttonSave => 'Save'; 73 + 74 + @override 75 + String get buttonSaveChanges => 'Save Changes'; 76 + 77 + @override 48 78 String get buttonSignIn => 'Sign In'; 49 79 50 80 @override 51 81 String get buttonShowContent => 'Show content'; 82 + 83 + @override 84 + String get buttonShare => 'Share'; 52 85 53 86 @override 54 87 String get buttonTryAgain => 'Try again'; ··· 60 93 String get commonNone => 'None'; 61 94 62 95 @override 96 + String get commonNow => 'now'; 97 + 98 + @override 99 + String get commonJustNow => 'Just now'; 100 + 101 + @override 63 102 String get commonNotCheckedYet => 'Not checked yet'; 64 103 65 104 @override 66 105 String get commonOff => 'Off'; 106 + 107 + @override 108 + String get commonUnknown => 'unknown'; 67 109 68 110 @override 69 111 String get dialogClearCacheContent => ··· 73 115 String get dialogClearCacheTitle => 'Clear cache?'; 74 116 75 117 @override 118 + String get dialogClearLocalBookmarksContent => 119 + 'This removes only local bookmarks from this device. Bluesky cloud bookmarks will not be deleted.'; 120 + 121 + @override 122 + String get dialogClearLocalBookmarksTitle => 'Clear local bookmarks?'; 123 + 124 + @override 125 + String get dialogDeletePostContent => 'This action cannot be undone.'; 126 + 127 + @override 128 + String get dialogDeletePostTitle => 'Delete Post?'; 129 + 130 + @override 131 + String get dialogDeleteDraftTitle => 'Delete Draft?'; 132 + 133 + @override 134 + String get dialogDiscardChangesContent => 'You have unsaved edits. Discard them and leave?'; 135 + 136 + @override 137 + String get dialogDiscardChangesTitle => 'Discard Changes?'; 138 + 139 + @override 140 + String get dialogEditAlgorithmContent => 141 + 'Lazurite saves edits by deleting and recreating the post record with the same URI. During re-indexing, ranking, counters, and search visibility can shift, and updates may take time to appear everywhere.'; 142 + 143 + @override 144 + String get dialogEditAlgorithmTitle => 'How Post Editing Works'; 145 + 146 + @override 147 + String get dialogSaveDraftContent => 'You have unsaved content. Would you like to save it as a draft?'; 148 + 149 + @override 150 + String get dialogSaveDraftTitle => 'Save Draft?'; 151 + 152 + @override 76 153 String dialogRemoveAccountContent(String handle) { 77 154 return 'Remove @$handle from this device?'; 78 155 } ··· 108 185 String get errorGenericTitle => 'Something went wrong'; 109 186 110 187 @override 188 + String get errorFailedToLoadBookmarks => 'Failed to load bookmarks'; 189 + 190 + @override 191 + String get errorFailedToLoadLikedPosts => 'Failed to load liked posts'; 192 + 193 + @override 194 + String errorFailedToLoadLikedPostsDetails(Object error) { 195 + return 'Failed to load liked posts: $error'; 196 + } 197 + 198 + @override 199 + String errorFailedToRefreshLikedPosts(Object error) { 200 + return 'Failed to refresh liked posts: $error'; 201 + } 202 + 203 + @override 204 + String get errorFailedToLoadTrending => 'Failed to load trending'; 205 + 206 + @override 207 + String errorFailedToLoadTrendingTopics(Object error) { 208 + return 'Failed to load trending topics: $error'; 209 + } 210 + 211 + @override 212 + String get errorUnknown => 'Unknown error'; 213 + 214 + @override 111 215 String get errorUnableToRemoveAccount => 'Unable to remove account right now.'; 112 216 113 217 @override ··· 147 251 } 148 252 149 253 @override 254 + String formatLikedOn(String date) { 255 + return 'Liked on $date'; 256 + } 257 + 258 + @override 259 + String formatLikesCount(int count) { 260 + String _temp0 = intl.Intl.pluralLogic(count, locale: localeName, other: '$count Likes', one: '1 Like'); 261 + return '$_temp0'; 262 + } 263 + 264 + @override 265 + String formatOfflineReconnectAction(String action) { 266 + return 'You are offline. Reconnect to $action.'; 267 + } 268 + 269 + @override 270 + String formatReplyingToHandle(String handle) { 271 + return 'Replying to @$handle'; 272 + } 273 + 274 + @override 275 + String formatRepostsCount(int count) { 276 + String _temp0 = intl.Intl.pluralLogic(count, locale: localeName, other: '$count Reposts', one: '1 Repost'); 277 + return '$_temp0'; 278 + } 279 + 280 + @override 281 + String formatSavedOn(String date) { 282 + return 'Saved on $date'; 283 + } 284 + 285 + @override 286 + String formatTrendingCategory(String category) { 287 + return 'Category: $category'; 288 + } 289 + 290 + @override 291 + String formatTrendingPostCount(int count) { 292 + String _temp0 = intl.Intl.pluralLogic(count, locale: localeName, other: '$count posts', one: '1 post'); 293 + return '$_temp0'; 294 + } 295 + 296 + @override 297 + String formatViewHandle(String handle) { 298 + return 'View @$handle'; 299 + } 300 + 301 + @override 302 + String formatComposeFailedToPickImage(Object error) { 303 + return 'Failed to pick image: $error'; 304 + } 305 + 306 + @override 307 + String formatComposeFailedToPickVideo(Object error) { 308 + return 'Failed to pick video: $error'; 309 + } 310 + 311 + @override 312 + String formatComposeFailedToSaveChanges(Object error) { 313 + return 'Failed to save changes: $error'; 314 + } 315 + 316 + @override 317 + String formatComposeFailedToSubmitPost(Object error) { 318 + return 'Failed to submit post: $error'; 319 + } 320 + 321 + @override 322 + String formatComposeImageTooLarge(String fileName, String sizeMb) { 323 + return 'Image \"$fileName\" is $sizeMb MB - max 1 MB.'; 324 + } 325 + 326 + @override 327 + String formatComposeQuotingHandle(String handle) { 328 + return 'Quoting @$handle'; 329 + } 330 + 331 + @override 332 + String formatComposeScheduledFor(String dateTime) { 333 + return 'Scheduled for $dateTime'; 334 + } 335 + 336 + @override 337 + String formatComposeVideoReadyWithAltText(String altText) { 338 + return 'Ready - \"$altText\"'; 339 + } 340 + 341 + @override 342 + String formatComposeVideoTooLarge(String sizeMb) { 343 + return 'Video is $sizeMb MB - exceeds the 100 MB limit.'; 344 + } 345 + 346 + @override 347 + String formatDraftCount(int count) { 348 + String _temp0 = intl.Intl.pluralLogic(count, locale: localeName, other: '$count drafts', one: '1 draft'); 349 + return '$_temp0'; 350 + } 351 + 352 + @override 353 + String get actionLikeThisPost => 'like this post'; 354 + 355 + @override 356 + String get actionReplyToThisPost => 'reply to this post'; 357 + 358 + @override 359 + String get actionRepostThisPost => 'repost this post'; 360 + 361 + @override 362 + String get actionPublishYourPost => 'publish your post'; 363 + 364 + @override 150 365 String get labelAbout => 'About'; 151 366 152 367 @override ··· 198 413 String get labelBookmarksAndLikes => 'Bookmarks & Likes'; 199 414 200 415 @override 416 + String get labelBookmarkActions => 'Bookmark actions'; 417 + 418 + @override 419 + String get labelBookmarkedPost => 'Bookmarked Post'; 420 + 421 + @override 422 + String get labelBookmarks => 'Bookmarks'; 423 + 424 + @override 425 + String get labelBluesky => 'Bluesky'; 426 + 427 + @override 428 + String get labelAlt => 'ALT'; 429 + 430 + @override 201 431 String get labelCacheCleared => 'Cache cleared'; 202 432 203 433 @override ··· 205 435 206 436 @override 207 437 String get labelCleanFollows => 'Clean Follows'; 438 + 439 + @override 440 + String get labelClose => 'Close'; 208 441 209 442 @override 210 443 String get labelClearCache => 'Clear Cache'; ··· 301 534 302 535 @override 303 536 String get labelNewPost => 'New Post'; 537 + 538 + @override 539 + String get labelNow => 'NOW'; 304 540 305 541 @override 306 542 String get labelNotifications => 'Notifications'; ··· 393 629 String get labelClearUntil => 'Clear until'; 394 630 395 631 @override 632 + String get labelClearLocalBookmarks => 'Clear local bookmarks'; 633 + 634 + @override 635 + String get labelCopyLink => 'Copy Link'; 636 + 637 + @override 638 + String get labelDeletePost => 'Delete Post'; 639 + 640 + @override 641 + String get labelDeleteDraft => 'Delete draft'; 642 + 643 + @override 644 + String get labelEditPost => 'Edit Post'; 645 + 646 + @override 647 + String get labelLiked => 'Liked'; 648 + 649 + @override 650 + String get labelLikedBy => 'LIKED BY'; 651 + 652 + @override 653 + String get labelLikedPost => 'Liked Post'; 654 + 655 + @override 656 + String get labelMoreInfo => 'More info'; 657 + 658 + @override 659 + String get labelLocal => 'Local'; 660 + 661 + @override 662 + String get labelOpenPost => 'Open post'; 663 + 664 + @override 665 + String get labelQuotePost => 'Quote Post'; 666 + 667 + @override 668 + String get labelQuoteReposts => 'QUOTE / REPOSTS'; 669 + 670 + @override 671 + String get labelQuotes => 'Quotes'; 672 + 673 + @override 674 + String get labelRemoveFromBluesky => 'Remove from Bluesky'; 675 + 676 + @override 677 + String get labelRemoveLocalSave => 'Remove local save'; 678 + 679 + @override 680 + String get labelReportPost => 'Report Post'; 681 + 682 + @override 683 + String get labelRepost => 'Repost'; 684 + 685 + @override 686 + String get labelRepostedBy => 'REPOSTED BY'; 687 + 688 + @override 689 + String get labelReposts => 'Reposts'; 690 + 691 + @override 692 + String get labelSaveImage => 'Save image'; 693 + 694 + @override 695 + String get labelSaveLocally => 'Save locally'; 696 + 697 + @override 698 + String get labelSaveToBluesky => 'Save to Bluesky'; 699 + 700 + @override 701 + String get labelSchedule => 'Schedule'; 702 + 703 + @override 704 + String get labelScheduled => 'Scheduled'; 705 + 706 + @override 396 707 String get labelSavedAccounts => 'Saved accounts'; 397 708 398 709 @override ··· 400 711 401 712 @override 402 713 String get labelSemanticSearch => 'Semantic Search'; 714 + 715 + @override 716 + String get labelShowLikedUsers => 'Show Liked Users'; 717 + 718 + @override 719 + String get labelShowQuoteRepostList => 'Show Quote/Repost List'; 403 720 404 721 @override 405 722 String get labelSettings => 'Settings'; ··· 432 749 String get labelTroubleshooting => 'Troubleshooting'; 433 750 434 751 @override 752 + String get labelVideo => 'Video'; 753 + 754 + @override 755 + String get labelTopics => 'Topics'; 756 + 757 + @override 758 + String get labelTrending => 'Trending'; 759 + 760 + @override 761 + String get labelSuggested => 'Suggested'; 762 + 763 + @override 764 + String get labelSuggestedFollows => 'Suggested Follows'; 765 + 766 + @override 767 + String get labelUnrepost => 'Unrepost'; 768 + 769 + @override 435 770 String get labelTypeaheadProvider => 'Typeahead Provider'; 436 771 437 772 @override ··· 501 836 String get messageFeedsSubtitle => 'Manage pinned and saved feeds'; 502 837 503 838 @override 839 + String get messageLinkCopiedToClipboard => 'Link copied to clipboard'; 840 + 841 + @override 842 + String get messageLoadingTrendingTopics => 'Loading trending topics'; 843 + 844 + @override 504 845 String get messageForceNextXrpc401Subtitle => 505 846 'Debug-only: next network request returns Unauthorized to test token refresh'; 506 847 ··· 508 849 String get messageManageSemanticSearchSubtitle => 'Manage semantic search from Bookmarks & Likes -> Search'; 509 850 510 851 @override 852 + String get messageMetadataTemporarilyUnavailable => 'Metadata temporarily unavailable'; 853 + 854 + @override 511 855 String get messageModeratedContentCannotReveal => 'Hidden by your moderation settings and cannot be revealed here.'; 512 856 513 857 @override ··· 518 862 'Moderation/ranking can differ by provider. Verify health and recent fallback state.'; 519 863 520 864 @override 865 + String get messageLikedPostsUnavailable => 'Liked posts are unavailable right now.'; 866 + 867 + @override 868 + String get messageChangesSaved => 'Changes saved.'; 869 + 870 + @override 871 + String get messageComposeAddAltText => 'Add alt text'; 872 + 873 + @override 874 + String get messageComposeAddImage => 'Add image'; 875 + 876 + @override 877 + String get messageComposeAddVideo => 'Add video'; 878 + 879 + @override 880 + String get messageComposeClearScheduledTime => 'Clear scheduled time'; 881 + 882 + @override 883 + String get messageComposeDescribeImage => 'Describe the image'; 884 + 885 + @override 886 + String get messageComposeDescribeVideo => 'Describe the video'; 887 + 888 + @override 889 + String get messageComposeDraftSaved => 'Draft saved'; 890 + 891 + @override 892 + String get messageComposeDrafts => 'Drafts'; 893 + 894 + @override 895 + String get messageComposeEditNotice => 896 + 'Edits are saved by replacing the record while keeping this post URI. Ranking, counts, and visibility may shift while networks re-index.'; 897 + 898 + @override 899 + String get messageComposeImageAltTextTitle => 'Alt text'; 900 + 901 + @override 902 + String get messageComposeImageMaxCount => 'Maximum 4 images allowed'; 903 + 904 + @override 905 + String get messageComposeImageMustBeJpegPngWebp => 'Image must be JPEG, PNG, or WebP'; 906 + 907 + @override 908 + String get messageComposeImageMustBeUnder1Mb => 'Image must be smaller than 1MB'; 909 + 910 + @override 911 + String get messageComposeNoDraftsSaved => 'No drafts saved'; 912 + 913 + @override 914 + String get messageComposeNoText => '(No text)'; 915 + 916 + @override 917 + String get messageComposePlaceholder => 'What\'s on your mind?'; 918 + 919 + @override 920 + String get messageComposePreviewUnavailable => 'Preview unavailable'; 921 + 922 + @override 923 + String get messageComposeQuotingPost => 'Quoting post'; 924 + 925 + @override 926 + String get messageComposeRemoveExistingMediaBeforeVideo => 'Remove existing media before adding a video'; 927 + 928 + @override 929 + String get messageComposeRemoveImage => 'Remove image'; 930 + 931 + @override 932 + String get messageComposeRemoveQuotedPost => 'Remove quoted post'; 933 + 934 + @override 935 + String get messageComposeSaveDraft => 'Save draft'; 936 + 937 + @override 938 + String get messageComposeVideoAltTextTitle => 'Video alt text'; 939 + 940 + @override 941 + String get messageVideoCheckingUploadLimits => 'Checking upload limits...'; 942 + 943 + @override 944 + String get messageVideoDailyUploadLimitReached => 'Daily video upload limit reached.'; 945 + 946 + @override 947 + String get messageVideoProcessing => 'Processing...'; 948 + 949 + @override 950 + String get messageVideoProcessingFailed => 'Video processing failed.'; 951 + 952 + @override 953 + String get messageVideoProcessingTimedOut => 'Video processing timed out.'; 954 + 955 + @override 956 + String get messageVideoReady => 'Ready'; 957 + 958 + @override 959 + String get messageVideoReadyToUpload => 'Ready to upload'; 960 + 961 + @override 962 + String get messageVideoUploadFailed => 'Upload failed - please try again.'; 963 + 964 + @override 965 + String get messageVideoUploading => 'Uploading...'; 966 + 967 + @override 968 + String get errorComposeChangedElsewhere => 'This post was changed elsewhere. Reopen it and try editing again.'; 969 + 970 + @override 971 + String get errorComposeCouldNotConfirmEdit => 972 + 'Edit was submitted but could not be confirmed yet. Please reopen the post and verify.'; 973 + 974 + @override 975 + String get errorComposeCouldNotSaveAndConfirmRecovery => 976 + 'Could not save changes and we could not confirm recovery. Reopen the thread and verify the post.'; 977 + 978 + @override 979 + String get errorComposeEditContextMissing => 'Edit context is missing. Please reopen the editor and try again.'; 980 + 981 + @override 982 + String get errorComposeFailedToCreatePost => 'Failed to create post. Please try again.'; 983 + 984 + @override 985 + String get errorComposeFailedToSaveChanges => 'Failed to save changes. Please try again.'; 986 + 987 + @override 988 + String get errorComposeFailedToUploadImage => 'Failed to upload image. Please try again.'; 989 + 990 + @override 991 + String get errorComposeImageFileNotFound => 'Image file not found. Please re-attach and try again.'; 992 + 993 + @override 994 + String get errorComposeNetworkSavedAsDraft => 'Network error - post saved as draft.'; 995 + 996 + @override 997 + String get errorComposeOriginalPostRestored => 'Could not save changes. Your original post was restored.'; 998 + 999 + @override 1000 + String get errorComposeUnsupportedImageFormat => 'Unsupported image format. Use JPEG, PNG, or WebP.'; 1001 + 1002 + @override 1003 + String get messageNoBookmarks => 'No bookmarks'; 1004 + 1005 + @override 1006 + String get messageNoBookmarksSubtitle => 'Posts you bookmark will appear here'; 1007 + 1008 + @override 1009 + String get messageNoBookmarksInSource => 'No bookmarks in this source'; 1010 + 1011 + @override 1012 + String get messageNoBookmarksInSourceSubtitle => 'Try switching tabs or saving posts to this source'; 1013 + 1014 + @override 1015 + String get messageNoInteractionsYet => 'No interactions yet'; 1016 + 1017 + @override 1018 + String get messageNoLikedPosts => 'No liked posts'; 1019 + 1020 + @override 1021 + String get messageNoLikedPostsSubtitle => 'Posts you like will appear here after sync'; 1022 + 1023 + @override 1024 + String get messageNoLikedPostsYet => 'No liked posts yet'; 1025 + 1026 + @override 1027 + String get messageNoQuotesYet => 'No quotes yet'; 1028 + 1029 + @override 1030 + String get messageNoRepostsYet => 'No reposts yet'; 1031 + 1032 + @override 1033 + String get messageNoTrendingTopicsRightNow => 'No trending topics right now'; 1034 + 1035 + @override 1036 + String get messagePostDeleted => 'Post deleted'; 1037 + 1038 + @override 1039 + String get messageQuotePostSubtitle => 'Quote this post with your own text'; 1040 + 1041 + @override 1042 + String get messageQuotedPostBlocked => 'Quoted post is blocked'; 1043 + 1044 + @override 1045 + String get messageQuotedPostNotFound => 'Quoted post not found'; 1046 + 1047 + @override 1048 + String get messageQuotedPostUnavailable => 'Quoted post is unavailable'; 1049 + 1050 + @override 1051 + String get messageRemoveRepostSubtitle => 'Remove this repost'; 1052 + 1053 + @override 1054 + String get messageReplyInThread => 'Reply in a thread'; 1055 + 1056 + @override 1057 + String get messageReplyingTo => 'Replying to'; 1058 + 1059 + @override 1060 + String get messageShareThisPostSubtitle => 'Share this post'; 1061 + 1062 + @override 1063 + String get messageShowLikedUsersSubtitle => 'View who liked this post'; 1064 + 1065 + @override 1066 + String get messageShowQuoteRepostListSubtitle => 'View quote posts and expand reposts'; 1067 + 1068 + @override 521 1069 String get messageRefreshProviderHealthSubtitle => 'Probe public AppView endpoints now'; 522 1070 523 1071 @override ··· 562 1110 String get messageSearchPeoplePlaceholder => 'Search people'; 563 1111 564 1112 @override 1113 + String get messageSearchForPeoplePlaceholder => 'Search for people'; 1114 + 1115 + @override 565 1116 String get messageSearchFeedsPlaceholder => 'Search feeds'; 566 1117 567 1118 @override ··· 590 1141 String get promptHandleOrDid => 'Handle or DID'; 591 1142 592 1143 @override 1144 + String get buttonAddFeed => 'Add feed'; 1145 + 1146 + @override 1147 + String get buttonAddMembers => 'Add members'; 1148 + 1149 + @override 1150 + String get buttonBlock => 'Block'; 1151 + 1152 + @override 1153 + String get buttonCreate => 'Create'; 1154 + 1155 + @override 1156 + String get buttonEdit => 'Edit'; 1157 + 1158 + @override 1159 + String get buttonFollow => 'Follow'; 1160 + 1161 + @override 1162 + String get buttonFollowAll => 'Follow all'; 1163 + 1164 + @override 1165 + String get buttonFollowing => 'Following'; 1166 + 1167 + @override 1168 + String get buttonFollowingInProgress => 'Following…'; 1169 + 1170 + @override 1171 + String get buttonLoadMore => 'Load more'; 1172 + 1173 + @override 1174 + String get buttonMute => 'Mute'; 1175 + 1176 + @override 1177 + String get buttonScan => 'Scan'; 1178 + 1179 + @override 1180 + String get buttonSeeAll => 'See all'; 1181 + 1182 + @override 1183 + String get buttonShowAccounts => 'Show accounts'; 1184 + 1185 + @override 1186 + String get buttonSubmitReport => 'Submit Report'; 1187 + 1188 + @override 1189 + String get buttonUnblock => 'Unblock'; 1190 + 1191 + @override 1192 + String get buttonUnfollow => 'Unfollow'; 1193 + 1194 + @override 1195 + String buttonUnfollowSelected(int count) { 1196 + return 'Unfollow Selected ($count)'; 1197 + } 1198 + 1199 + @override 1200 + String get buttonUnmute => 'Unmute'; 1201 + 1202 + @override 1203 + String get dialogBlockAccountContent => 1204 + 'They will not be able to see your posts or interact with you. They will not be notified that you blocked them.'; 1205 + 1206 + @override 1207 + String get dialogBlockAccountTitle => 'Block Account?'; 1208 + 1209 + @override 1210 + String get dialogDeleteListTitle => 'Delete list?'; 1211 + 1212 + @override 1213 + String get dialogDeleteStarterPackContent => 1214 + 'This will permanently delete this starter pack and its backing list. This cannot be undone.'; 1215 + 1216 + @override 1217 + String get dialogDeleteStarterPackTitle => 'Delete starter pack'; 1218 + 1219 + @override 1220 + String get dialogMuteAccountContent => 'You will no longer see their posts or receive notifications from them.'; 1221 + 1222 + @override 1223 + String get dialogMuteAccountTitle => 'Mute Account?'; 1224 + 1225 + @override 1226 + String get dialogUnblockAccountContent => 'They will be able to see your posts and interact with you again.'; 1227 + 1228 + @override 1229 + String get dialogUnblockAccountTitle => 'Unblock Account?'; 1230 + 1231 + @override 1232 + String get dialogUnfollowAccountContent => 'You will no longer see their posts in your feed.'; 1233 + 1234 + @override 1235 + String get dialogUnfollowAccountTitle => 'Unfollow?'; 1236 + 1237 + @override 1238 + String get dialogUnmuteAccountContent => 'You will see their posts and receive notifications again.'; 1239 + 1240 + @override 1241 + String get dialogUnmuteAccountTitle => 'Unmute Account?'; 1242 + 1243 + @override 1244 + String get errorFailedToCreateStarterPack => 'Failed to create starter pack'; 1245 + 1246 + @override 1247 + String get errorFailedToLoadAccounts => 'Failed to load accounts'; 1248 + 1249 + @override 1250 + String get errorFailedToLoadFeed => 'Failed to load feed'; 1251 + 1252 + @override 1253 + String get errorFailedToLoadFeeds => 'Failed to load feeds'; 1254 + 1255 + @override 1256 + String get errorFailedToLoadList => 'Failed to load list'; 1257 + 1258 + @override 1259 + String get errorFailedToLoadLists => 'Failed to load lists'; 1260 + 1261 + @override 1262 + String get errorFailedToLoadMembers => 'Failed to load members'; 1263 + 1264 + @override 1265 + String get errorFailedToLoadMore => 'Failed to load more'; 1266 + 1267 + @override 1268 + String get errorFailedToLoadPosts => 'Failed to load posts'; 1269 + 1270 + @override 1271 + String get errorFailedToLoadProfile => 'Unable to load profile'; 1272 + 1273 + @override 1274 + String get errorFailedToLoadStarterPack => 'Failed to load starter pack'; 1275 + 1276 + @override 1277 + String get errorFailedToLoadStarterPacks => 'Failed to load starter packs'; 1278 + 1279 + @override 1280 + String get errorFailedToLoadSuggestions => 'Failed to load suggestions'; 1281 + 1282 + @override 1283 + String get errorFollowAuditFailed => 'Failed to complete follow audit.'; 1284 + 1285 + @override 1286 + String get errorImageTooLarge => 'Image must be smaller than 1MB'; 1287 + 1288 + @override 1289 + String get errorInvalidProfileImageType => 'Use a JPEG or PNG image'; 1290 + 1291 + @override 1292 + String get errorProfileImageReadFailed => 'Unable to read selected image'; 1293 + 1294 + @override 1295 + String get errorReportFailed => 'Unable to submit your report. Please try again later.'; 1296 + 1297 + @override 1298 + String get errorReportFailedTitle => 'Report Failed'; 1299 + 1300 + @override 1301 + String errorUnableToLoadConnections(String tab) { 1302 + return 'Unable to load $tab'; 1303 + } 1304 + 1305 + @override 1306 + String get errorUnableToUpdateProfile => 'Unable to update profile'; 1307 + 1308 + @override 1309 + String formatAccountCount(int count) { 1310 + String _temp0 = intl.Intl.pluralLogic(count, locale: localeName, other: '$count accounts', one: '1 account'); 1311 + return '$_temp0'; 1312 + } 1313 + 1314 + @override 1315 + String formatBlockedByAccountsUnavailable(int count) { 1316 + String _temp0 = intl.Intl.pluralLogic( 1317 + count, 1318 + locale: localeName, 1319 + other: '$count blocked-by accounts', 1320 + one: '1 blocked-by account', 1321 + ); 1322 + return 'Found $_temp0, but public Bluesky profile details could not be loaded.'; 1323 + } 1324 + 1325 + @override 1326 + String formatConnectionsLoading(String tab) { 1327 + return 'Loading $tab...'; 1328 + } 1329 + 1330 + @override 1331 + String formatConnectionsNoMatches(String tab, String query) { 1332 + return 'No $tab match \"$query\"'; 1333 + } 1334 + 1335 + @override 1336 + String formatConnectionsNoneFound(String tab) { 1337 + return 'No $tab found'; 1338 + } 1339 + 1340 + @override 1341 + String formatConnectionsSearching(int count) { 1342 + return 'Searching $count accounts...'; 1343 + } 1344 + 1345 + @override 1346 + String formatConnectionsSearched(int count) { 1347 + return 'Searched $count accounts'; 1348 + } 1349 + 1350 + @override 1351 + String formatConnectionsSearchStopped(int count) { 1352 + return 'Search stopped after $count accounts'; 1353 + } 1354 + 1355 + @override 1356 + String formatClassifyingProgress(int progress, int total) { 1357 + return 'Classifying: $progress/$total'; 1358 + } 1359 + 1360 + @override 1361 + String get formatDidCopied => 'DID copied to clipboard'; 1362 + 1363 + @override 1364 + String formatFetchingFollowsProgress(int progress, int total) { 1365 + return 'Fetching follows: $progress/$total'; 1366 + } 1367 + 1368 + @override 1369 + String formatFollowedMemberCount(int count) { 1370 + String _temp0 = intl.Intl.pluralLogic(count, locale: localeName, other: '$count members', one: '1 member'); 1371 + return 'Followed $_temp0'; 1372 + } 1373 + 1374 + @override 1375 + String formatFollowsScanned(int count) { 1376 + String _temp0 = intl.Intl.pluralLogic( 1377 + count, 1378 + locale: localeName, 1379 + other: '$count follows scanned for problematic accounts', 1380 + one: '1 follow scanned for problematic accounts', 1381 + ); 1382 + return '$_temp0'; 1383 + } 1384 + 1385 + @override 1386 + String formatHideStatus(String status) { 1387 + return 'Hide $status'; 1388 + } 1389 + 1390 + @override 1391 + String formatJoinedDate(String date) { 1392 + return 'Joined $date'; 1393 + } 1394 + 1395 + @override 1396 + String formatJoinedRelative(String relativeTime) { 1397 + return 'Joined $relativeTime'; 1398 + } 1399 + 1400 + @override 1401 + String formatListByHandle(String handle) { 1402 + return 'by @$handle'; 1403 + } 1404 + 1405 + @override 1406 + String formatMemberCount(int count) { 1407 + String _temp0 = intl.Intl.pluralLogic(count, locale: localeName, other: '$count members', one: '1 member'); 1408 + return '$_temp0'; 1409 + } 1410 + 1411 + @override 1412 + String formatProfileReportTitle(String title, String handle) { 1413 + return '$title by @$handle'; 1414 + } 1415 + 1416 + @override 1417 + String formatProfileTextLimit(String label, int count) { 1418 + return '$label must be $count characters or fewer'; 1419 + } 1420 + 1421 + @override 1422 + String formatProfileTextTooLong(String label) { 1423 + return '$label is too long'; 1424 + } 1425 + 1426 + @override 1427 + String formatProfilesFailedToLoad(int count) { 1428 + String _temp0 = intl.Intl.pluralLogic( 1429 + count, 1430 + locale: localeName, 1431 + other: '$count profiles could not be loaded.', 1432 + one: '1 profile could not be loaded.', 1433 + ); 1434 + return '$_temp0'; 1435 + } 1436 + 1437 + @override 1438 + String formatReportSubmitted(String reportId) { 1439 + return 'Thank you. Your report (ID: $reportId) has been submitted.'; 1440 + } 1441 + 1442 + @override 1443 + String formatSelectedCount(int selected, int total) { 1444 + return 'Selected: $selected/$total'; 1445 + } 1446 + 1447 + @override 1448 + String formatShowStatus(String status) { 1449 + return 'Show $status'; 1450 + } 1451 + 1452 + @override 1453 + String formatUnavailableAccounts(int count) { 1454 + return 'Unavailable accounts ($count)'; 1455 + } 1456 + 1457 + @override 1458 + String formatUnfollowedAccounts(int count) { 1459 + return 'Unfollowed $count account(s)'; 1460 + } 1461 + 1462 + @override 1463 + String formatValidationRequiredMaxCharacters(int count) { 1464 + return 'Required, max $count characters'; 1465 + } 1466 + 1467 + @override 1468 + String get labelAddToList => 'Add to list'; 1469 + 1470 + @override 1471 + String get labelAuditFollowers => 'Audit Followers'; 1472 + 1473 + @override 1474 + String get labelBanner => 'Banner'; 1475 + 1476 + @override 1477 + String get labelBlockViaList => 'Block via list'; 1478 + 1479 + @override 1480 + String get labelBlockedBy => 'Blocked By'; 1481 + 1482 + @override 1483 + String get labelBlocking => 'Blocking'; 1484 + 1485 + @override 1486 + String get labelConnections => 'Connections'; 1487 + 1488 + @override 1489 + String get labelCopyDid => 'Copy DID'; 1490 + 1491 + @override 1492 + String get labelCreateList => 'Create list'; 1493 + 1494 + @override 1495 + String get labelCreateStarterPack => 'Create starter pack'; 1496 + 1497 + @override 1498 + String get labelCurateShort => 'CURATE'; 1499 + 1500 + @override 1501 + String get labelCurrentMembers => 'Current Members'; 1502 + 1503 + @override 1504 + String get labelCurationLists => 'Curation Lists'; 1505 + 1506 + @override 1507 + String get labelDescription => 'Description'; 1508 + 1509 + @override 1510 + String get labelDescriptionOptional => 'Description (optional)'; 1511 + 1512 + @override 1513 + String get labelDisplayName => 'Display name'; 1514 + 1515 + @override 1516 + String get labelEditList => 'Edit list'; 1517 + 1518 + @override 1519 + String get labelEditProfile => 'Edit profile'; 1520 + 1521 + @override 1522 + String get labelEditStarterPack => 'Edit starter pack'; 1523 + 1524 + @override 1525 + String get labelFeed => 'Feed'; 1526 + 1527 + @override 1528 + String get labelFollowers => 'Followers'; 1529 + 1530 + @override 1531 + String get labelJoinedThisWeek => 'joined this week'; 1532 + 1533 + @override 1534 + String get labelJoinedTotal => 'joined total'; 1535 + 1536 + @override 1537 + String get labelFollowing => 'Following'; 1538 + 1539 + @override 1540 + String get labelList => 'List'; 1541 + 1542 + @override 1543 + String get labelLists => 'Lists'; 1544 + 1545 + @override 1546 + String get labelMedia => 'Media'; 1547 + 1548 + @override 1549 + String get labelMembers => 'Members'; 1550 + 1551 + @override 1552 + String get labelModerationLists => 'Moderation Lists'; 1553 + 1554 + @override 1555 + String get labelModerationShort => 'MOD'; 1556 + 1557 + @override 1558 + String get labelMuteList => 'Mute list'; 1559 + 1560 + @override 1561 + String get labelMutuals => 'Mutuals'; 1562 + 1563 + @override 1564 + String get labelMyLists => 'My Lists'; 1565 + 1566 + @override 1567 + String get labelName => 'Name'; 1568 + 1569 + @override 1570 + String get labelNewStarterPack => 'New Starter Pack'; 1571 + 1572 + @override 1573 + String get labelOtherLists => 'Other Lists'; 1574 + 1575 + @override 1576 + String get labelPronouns => 'Pronouns'; 1577 + 1578 + @override 1579 + String get labelProfileContext => 'Profile Context'; 1580 + 1581 + @override 1582 + String get labelProfileTitle => 'Profile'; 1583 + 1584 + @override 1585 + String get labelRecommendedFeeds => 'Recommended Feeds'; 1586 + 1587 + @override 1588 + String get labelReferenceLists => 'Reference Lists'; 1589 + 1590 + @override 1591 + String get labelReferenceShort => 'REFERENCE'; 1592 + 1593 + @override 1594 + String get labelReplies => 'Replies'; 1595 + 1596 + @override 1597 + String get labelReport => 'Report'; 1598 + 1599 + @override 1600 + String get labelReportAccount => 'Report Account'; 1601 + 1602 + @override 1603 + String get labelReportReason => 'Reason'; 1604 + 1605 + @override 1606 + String get labelReportReasonExplanationRequired => 'Explanation (required)'; 1607 + 1608 + @override 1609 + String get labelReportReasonHarassment => 'Harassment'; 1610 + 1611 + @override 1612 + String get labelReportReasonMisleading => 'Misleading'; 1613 + 1614 + @override 1615 + String get labelReportReasonOther => 'Other'; 1616 + 1617 + @override 1618 + String get labelReportReasonSexualContent => 'Sexual Content'; 1619 + 1620 + @override 1621 + String get labelReportReasonSpam => 'Spam'; 1622 + 1623 + @override 1624 + String get labelReportReasonViolation => 'Violation'; 1625 + 1626 + @override 1627 + String get labelReportSubmitted => 'Report Submitted'; 1628 + 1629 + @override 1630 + String get labelSelectAll => 'Select All'; 1631 + 1632 + @override 1633 + String get labelSelectFeed => 'Select a feed'; 1634 + 1635 + @override 1636 + String get labelShareProfile => 'Share Profile'; 1637 + 1638 + @override 1639 + String get labelStarterPack => 'Starter Pack'; 1640 + 1641 + @override 1642 + String get labelType => 'Type'; 1643 + 1644 + @override 1645 + String get labelTotalJoined => 'total joined'; 1646 + 1647 + @override 1648 + String get labelUnavailableLikedPost => 'Unavailable liked post'; 1649 + 1650 + @override 1651 + String get labelUnblockViaList => 'Unblock via list'; 1652 + 1653 + @override 1654 + String get labelUnmuteList => 'Unmute list'; 1655 + 1656 + @override 1657 + String get labelUpToThree => '(up to 3)'; 1658 + 1659 + @override 1660 + String get labelWebsite => 'Website'; 1661 + 1662 + @override 1663 + String get labelYou => 'You'; 1664 + 1665 + @override 1666 + String get messageBlockedByContextNotice => 1667 + 'Blocks are a normal part of social media. This data is public on the AT Protocol.'; 1668 + 1669 + @override 1670 + String get messageBlockingOnlyOwnProfile => 'Blocking information is only available when viewing your own profile.'; 1671 + 1672 + @override 1673 + String get messageChangeAvatarImage => 'Change avatar image'; 1674 + 1675 + @override 1676 + String get messageChangeBannerImage => 'Change banner image'; 1677 + 1678 + @override 1679 + String get messageEnterValidWebsite => 'Enter a valid website'; 1680 + 1681 + @override 1682 + String get messageFeedUnavailableForModerationLists => 'Feed not available for moderation lists'; 1683 + 1684 + @override 1685 + String get messageFollowAuditIntro => 'Scan your follows for deleted, suspended, blocked, and hidden accounts.'; 1686 + 1687 + @override 1688 + String get messageFollowAuditStartPrompt => 'Tap Scan to audit your follow list.'; 1689 + 1690 + @override 1691 + String get messageNoAccountsBlockedThisUser => 'No accounts have blocked this user'; 1692 + 1693 + @override 1694 + String get messageNoListsYet => 'No lists yet'; 1695 + 1696 + @override 1697 + String get messageNoMembers => 'No members'; 1698 + 1699 + @override 1700 + String get messageNoMembersYet => 'No members yet'; 1701 + 1702 + @override 1703 + String get messageNoMembersYetSearch => 'No members yet. Search above to add people.'; 1704 + 1705 + @override 1706 + String get messageNoMediaPostsYet => 'No media posts yet'; 1707 + 1708 + @override 1709 + String get messageNoPostsYet => 'No posts yet'; 1710 + 1711 + @override 1712 + String get messageNoProblematicFollows => 'No problematic follows found'; 1713 + 1714 + @override 1715 + String get messageNoRepliesYet => 'No replies yet'; 1716 + 1717 + @override 1718 + String get messageNoResultsForFilters => 'No results visible for the current filters.'; 1719 + 1720 + @override 1721 + String get messageNoStarterPacksYet => 'No starter packs yet'; 1722 + 1723 + @override 1724 + String get messageNoSuggestionsFound => 'No suggestions found'; 1725 + 1726 + @override 1727 + String get messageNotBlockingAnyone => 'Not blocking anyone'; 1728 + 1729 + @override 1730 + String get messageNotOnAnyLists => 'Not on any lists'; 1731 + 1732 + @override 1733 + String get messageProfileUnavailable => 'Profile unavailable'; 1734 + 1735 + @override 1736 + String get messageProfileUpdated => 'Profile updated'; 1737 + 1738 + @override 1739 + String get messageReportExplanationHint => 'Please explain why you are reporting this...'; 1740 + 1741 + @override 1742 + String get messageReportReasonHarassmentDescription => 'Harassment or rude behaviour'; 1743 + 1744 + @override 1745 + String get messageReportReasonMisleadingDescription => 'Misleading or deceptive content'; 1746 + 1747 + @override 1748 + String get messageReportReasonOtherDescription => 'Other reason (requires explanation)'; 1749 + 1750 + @override 1751 + String get messageReportReasonSexualContentDescription => 'Unwanted sexual content'; 1752 + 1753 + @override 1754 + String get messageReportReasonSpamDescription => 'Spam or unsolicited content'; 1755 + 1756 + @override 1757 + String get messageReportReasonViolationDescription => 'Violates community guidelines'; 1758 + 1759 + @override 1760 + String get messageSearchConnectionsPlaceholder => 'Search handle, name, or description'; 1761 + 1762 + @override 1763 + String get messageSearchPeopleToAddPlaceholder => 'Search for people to add'; 1764 + 1765 + @override 1766 + String get messageSomeBlockedAccountsUnavailable => 'Some blocked accounts are suspended or unavailable.'; 1767 + 1768 + @override 1769 + String get messageSuggestedFollowsUnavailable => 'Suggested follows are unavailable right now.'; 1770 + 1771 + @override 1772 + String get messageUnavailableAccountsDescription => 1773 + 'These accounts are suspended or their public profile could not be fetched.'; 1774 + 1775 + @override 1776 + String get statusBlockedBy => 'Blocked by'; 1777 + 1778 + @override 1779 + String get statusBlocking => 'Blocking'; 1780 + 1781 + @override 1782 + String get statusDeactivated => 'Deactivated'; 1783 + 1784 + @override 1785 + String get statusDeleted => 'Deleted'; 1786 + 1787 + @override 1788 + String get statusHidden => 'Hidden'; 1789 + 1790 + @override 1791 + String get statusMutualBlock => 'Mutual block'; 1792 + 1793 + @override 1794 + String get statusSelfFollow => 'Self-follow'; 1795 + 1796 + @override 1797 + String get statusSuspended => 'Suspended'; 1798 + 1799 + @override 1800 + String get tooltipClearSearch => 'Clear search'; 1801 + 1802 + @override 1803 + String get tooltipJumpToTop => 'Jump to top'; 1804 + 1805 + @override 1806 + String get validationEnterValidWebsite => 'Enter a valid website'; 1807 + 1808 + @override 1809 + String get accountSwitcherNoOtherAccounts => 1810 + 'No other signed-in accounts yet. Add an account to switch between profiles.'; 1811 + 1812 + @override 1813 + String get buttonAddAccount => 'Add Account'; 1814 + 1815 + @override 1816 + String get buttonCopyAll => 'Copy All'; 1817 + 1818 + @override 1819 + String get buttonMarkAllRead => 'Mark All Read'; 1820 + 1821 + @override 1822 + String get errorFailedToAddAccount => 'Failed to add account'; 1823 + 1824 + @override 1825 + String get errorFailedToLoadMessages => 'Failed to load messages'; 1826 + 1827 + @override 1828 + String get errorFailedToLoadNotifications => 'Failed to load notifications'; 1829 + 1830 + @override 1831 + String get errorUnableToRemoveAccountNow => 'Unable to remove account right now.'; 1832 + 1833 + @override 1834 + String formatActorListTwo(String first, String second) { 1835 + return '$first and $second'; 1836 + } 1837 + 1838 + @override 1839 + String formatActorListWithOthers(String first, String second, int count) { 1840 + return '$first, $second, and $count others'; 1841 + } 1842 + 1843 + @override 1844 + String formatMonthDay(String month, int day) { 1845 + return '$month $day'; 1846 + } 1847 + 1848 + @override 1849 + String formatRemoveAccountContent(String handle) { 1850 + return 'Remove @$handle from this device?'; 1851 + } 1852 + 1853 + @override 1854 + String get labelAccounts => 'Accounts'; 1855 + 1856 + @override 1857 + String get labelAlertsTitle => 'Alerts'; 1858 + 1859 + @override 1860 + String get labelConversation => 'Conversation'; 1861 + 1862 + @override 1863 + String get labelFollows => 'Follows'; 1864 + 1865 + @override 1866 + String get labelLikes => 'Likes'; 1867 + 1868 + @override 1869 + String get labelMessageRequests => 'Requests'; 1870 + 1871 + @override 1872 + String get labelOther => 'Other'; 1873 + 1874 + @override 1875 + String get labelPrimary => 'Primary'; 1876 + 1877 + @override 1878 + String get labelSomeone => 'Someone'; 1879 + 1880 + @override 1881 + String get messageCopied => 'Message copied'; 1882 + 1883 + @override 1884 + String get messageDeleted => 'Message deleted'; 1885 + 1886 + @override 1887 + String get messageLocalNotificationFallbackBody => 'sent a notification'; 1888 + 1889 + @override 1890 + String get messageNewNotification => 'New notification'; 1891 + 1892 + @override 1893 + String get messageNoConnection => 'No connection'; 1894 + 1895 + @override 1896 + String get messageNoConversationsYet => 'No conversations yet'; 1897 + 1898 + @override 1899 + String get messageNoMessageRequests => 'No message requests'; 1900 + 1901 + @override 1902 + String get messageNoMessagesYet => 'No messages yet'; 1903 + 1904 + @override 1905 + String get messageNoNotificationsYet => 'No notifications yet'; 1906 + 1907 + @override 1908 + String get messageNotificationContactMatch => 'joined from your contacts'; 1909 + 1910 + @override 1911 + String get messageNotificationFollow => 'followed you'; 1912 + 1913 + @override 1914 + String get messageNotificationInteracted => 'interacted with you'; 1915 + 1916 + @override 1917 + String get messageNotificationLike => 'liked your post'; 1918 + 1919 + @override 1920 + String get messageNotificationLikeViaRepost => 'liked your repost'; 1921 + 1922 + @override 1923 + String get messageNotificationMention => 'mentioned you'; 1924 + 1925 + @override 1926 + String get messageNotificationQuote => 'quoted your post'; 1927 + 1928 + @override 1929 + String get messageNotificationReply => 'replied to your post'; 1930 + 1931 + @override 1932 + String get messageNotificationRepost => 'reposted your post'; 1933 + 1934 + @override 1935 + String get messageNotificationRepostViaRepost => 'reposted your repost'; 1936 + 1937 + @override 1938 + String get messageNotificationStarterPackJoined => 'joined via your starter pack'; 1939 + 1940 + @override 1941 + String get messageNotificationSubscribedPost => 'posted a new update'; 1942 + 1943 + @override 1944 + String get messageNotificationUnverified => 'removed your verification'; 1945 + 1946 + @override 1947 + String get messageNotificationVerified => 'verified your account'; 1948 + 1949 + @override 1950 + String get messagePlaceholder => 'Message…'; 1951 + 1952 + @override 1953 + String get messagePleaseSignInAgainForAccount => 'Please sign in again for that account.'; 1954 + 1955 + @override 1956 + String get messageReconnectToLoadMessages => 'Reconnect to load messages.'; 1957 + 1958 + @override 1959 + String get messageReconnectToLoadNotifications => 'Reconnect to load notifications.'; 1960 + 1961 + @override 1962 + String get messageThreadCopied => 'Thread copied'; 1963 + 1964 + @override 1965 + String get messageToday => 'Today'; 1966 + 1967 + @override 1968 + String get messageYesterday => 'Yesterday'; 1969 + 1970 + @override 1971 + String get placeholderUsernameBskySocial => 'username.bsky.social'; 1972 + 1973 + @override 1974 + String get validationEnterBlueskyHandleOrDid => 'Enter a Bluesky handle or DID'; 1975 + 1976 + @override 1977 + String get validationEnterCompleteDid => 'Enter a complete DID like did:plc:... or did:web:...'; 1978 + 1979 + @override 1980 + String get validationEnterFullHandle => 'Enter a full handle like username.bsky.social'; 1981 + 1982 + @override 1983 + String get validationUseSupportedDid => 'Use a did:plc:... or did:web:... identifier'; 1984 + 1985 + @override 593 1986 String get validationEnterAppPassword => 'Enter your app password'; 1987 + 1988 + @override 1989 + String get buttonAdd => 'Add'; 1990 + 1991 + @override 1992 + String get buttonAdding => 'Adding...'; 1993 + 1994 + @override 1995 + String get buttonInspiredByPdsLs => 'Inspired by pds.ls'; 1996 + 1997 + @override 1998 + String get buttonCopyJson => 'Copy JSON'; 1999 + 2000 + @override 2001 + String get buttonResolve => 'Resolve'; 2002 + 2003 + @override 2004 + String get buttonUnsubscribe => 'Unsubscribe'; 2005 + 2006 + @override 2007 + String get dialogAddLabelerTitle => 'Add labeler'; 2008 + 2009 + @override 2010 + String get dialogClearAllLogsContent => 'This will permanently delete all log files. This action cannot be undone.'; 2011 + 2012 + @override 2013 + String get dialogClearAllLogsTitle => 'Clear all logs?'; 2014 + 2015 + @override 2016 + String get errorFailedToLoadLogs => 'Failed to load logs'; 2017 + 2018 + @override 2019 + String get errorFailedToLoadModerationSettings => 'Failed to load moderation settings'; 2020 + 2021 + @override 2022 + String errorFailedToUnsubscribeLabeler(Object error) { 2023 + return 'Failed to unsubscribe: $error'; 2024 + } 2025 + 2026 + @override 2027 + String errorFailedToUpdateAdultContent(Object error) { 2028 + return 'Failed to update adult content: $error'; 2029 + } 2030 + 2031 + @override 2032 + String errorFailedToUpdateLabelPreference(Object error) { 2033 + return 'Failed to update preference: $error'; 2034 + } 2035 + 2036 + @override 2037 + String errorFailedToUpdateLabelerSubscription(Object error) { 2038 + return 'Failed to update subscription: $error'; 2039 + } 2040 + 2041 + @override 2042 + String get errorLabelerDidRequired => 'Enter a labeler DID.'; 2043 + 2044 + @override 2045 + String get errorLabelerNotFound => 'Labeler not found.'; 2046 + 2047 + @override 2048 + String get errorNoLabelerFoundForDid => 'No labeler found for that DID.'; 2049 + 2050 + @override 2051 + String get errorUnableToLoadLabeler => 'Unable to load labeler'; 2052 + 2053 + @override 2054 + String formatAddLabelerLimit(int current, int max) { 2055 + return 'Add ($current/$max)'; 2056 + } 2057 + 2058 + @override 2059 + String formatCollectionsCount(int count) { 2060 + String _temp0 = intl.Intl.pluralLogic(count, locale: localeName, other: '$count collections', one: '1 collection'); 2061 + return '$_temp0'; 2062 + } 2063 + 2064 + @override 2065 + String formatCid(String cid) { 2066 + return 'CID: $cid'; 2067 + } 2068 + 2069 + @override 2070 + String formatCustomLabelCount(int count) { 2071 + String _temp0 = intl.Intl.pluralLogic( 2072 + count, 2073 + locale: localeName, 2074 + other: '$count custom labels', 2075 + one: '1 custom label', 2076 + ); 2077 + return '$_temp0'; 2078 + } 2079 + 2080 + @override 2081 + String formatDefinitionCount(int count) { 2082 + String _temp0 = intl.Intl.pluralLogic(count, locale: localeName, other: '$count definitions', one: '1 definition'); 2083 + return '$_temp0'; 2084 + } 2085 + 2086 + @override 2087 + String formatLoadedRecordsCount(int count) { 2088 + return '$count loaded'; 2089 + } 2090 + 2091 + @override 2092 + String formatLoadedRecordsOfTotal(int loaded, int total) { 2093 + return '$loaded of $total'; 2094 + } 2095 + 2096 + @override 2097 + String formatModerationSourceLabel(String source) { 2098 + return '$source label'; 2099 + } 2100 + 2101 + @override 2102 + String formatPolicyBlur(String value) { 2103 + return 'Blur $value'; 2104 + } 2105 + 2106 + @override 2107 + String formatPolicyDefault(String value) { 2108 + return 'Default $value'; 2109 + } 2110 + 2111 + @override 2112 + String formatPolicyId(String identifier) { 2113 + return 'ID $identifier'; 2114 + } 2115 + 2116 + @override 2117 + String formatPolicySeverity(String value) { 2118 + return 'Severity $value'; 2119 + } 2120 + 2121 + @override 2122 + String formatPublishedValueCount(int count) { 2123 + String _temp0 = intl.Intl.pluralLogic( 2124 + count, 2125 + locale: localeName, 2126 + other: '$count published values', 2127 + one: '1 published value', 2128 + ); 2129 + return '$_temp0'; 2130 + } 2131 + 2132 + @override 2133 + String formatRecordsCount(int count) { 2134 + String _temp0 = intl.Intl.pluralLogic(count, locale: localeName, other: '$count records', one: '1 record'); 2135 + return '$_temp0'; 2136 + } 2137 + 2138 + @override 2139 + String formatSubscribedToLabeler(String name) { 2140 + return 'Subscribed to $name'; 2141 + } 2142 + 2143 + @override 2144 + String get labelAdultContentSetting => 'Adult content'; 2145 + 2146 + @override 2147 + String get labelAdultOnlyShort => '18+'; 2148 + 2149 + @override 2150 + String get labelAlwaysOn => 'Always on'; 2151 + 2152 + @override 2153 + String get labelAutoScroll => 'Auto-scroll'; 2154 + 2155 + @override 2156 + String get labelBlueskyModeration => 'Bluesky moderation'; 2157 + 2158 + @override 2159 + String get labelBuiltIn => 'Built-in'; 2160 + 2161 + @override 2162 + String get labelBuiltInLabeler => 'Built-in labeler'; 2163 + 2164 + @override 2165 + String get labelBuiltInModeration => 'Built-in moderation'; 2166 + 2167 + @override 2168 + String get labelBlockedAccount => 'Blocked account'; 2169 + 2170 + @override 2171 + String get labelBlockedByAccount => 'Blocked by account'; 2172 + 2173 + @override 2174 + String get labelBlockedRelationship => 'Blocked relationship'; 2175 + 2176 + @override 2177 + String get labelCollections => 'COLLECTIONS'; 2178 + 2179 + @override 2180 + String get labelContentPreferenceHide => 'Hide'; 2181 + 2182 + @override 2183 + String get labelContentPreferenceIgnore => 'Ignore'; 2184 + 2185 + @override 2186 + String get labelContentPreferenceWarn => 'Warn'; 2187 + 2188 + @override 2189 + String get labelCustomLabelers => 'Custom labelers'; 2190 + 2191 + @override 2192 + String get labelLabeler => 'Labeler'; 2193 + 2194 + @override 2195 + String get labelLabelerDid => 'Labeler DID'; 2196 + 2197 + @override 2198 + String get labelLabelersAndContentModeration => 'Labelers and content moderation'; 2199 + 2200 + @override 2201 + String get labelLabelPreferences => 'Label preferences'; 2202 + 2203 + @override 2204 + String get labelHiddenContent => 'Hidden content'; 2205 + 2206 + @override 2207 + String get labelLogLevelDebug => 'Debug'; 2208 + 2209 + @override 2210 + String get labelLogLevelError => 'Error'; 2211 + 2212 + @override 2213 + String get labelLogLevelFatal => 'Fatal'; 2214 + 2215 + @override 2216 + String get labelLogLevelInfo => 'Info'; 2217 + 2218 + @override 2219 + String get labelLogLevelTrace => 'Trace'; 2220 + 2221 + @override 2222 + String get labelLogLevelWarning => 'Warning'; 2223 + 2224 + @override 2225 + String get labelModerationNote => 'Moderation note'; 2226 + 2227 + @override 2228 + String get labelModerationSourceBluesky => 'Bluesky'; 2229 + 2230 + @override 2231 + String get labelModerationSourceSubscribedLabeler => 'Subscribed labeler'; 2232 + 2233 + @override 2234 + String get labelMutedAccount => 'Muted account'; 2235 + 2236 + @override 2237 + String get labelMutedPhrase => 'Muted phrase'; 2238 + 2239 + @override 2240 + String get labelNoCustomLabelDefinitions => 'No custom label definitions'; 2241 + 2242 + @override 2243 + String get labelNoCustomLabelers => 'No custom labelers'; 2244 + 2245 + @override 2246 + String get labelPdsExplorer => 'PDS Explorer'; 2247 + 2248 + @override 2249 + String get labelPublishedPolicies => 'Published policies'; 2250 + 2251 + @override 2252 + String get labelRecordJson => 'Record JSON'; 2253 + 2254 + @override 2255 + String get labelRefresh => 'Refresh'; 2256 + 2257 + @override 2258 + String get labelRepository => 'Repository'; 2259 + 2260 + @override 2261 + String get labelSubscribed => 'Subscribed'; 2262 + 2263 + @override 2264 + String get labelSensitiveContent => 'Sensitive content'; 2265 + 2266 + @override 2267 + String get labelUnknown => 'Unknown'; 2268 + 2269 + @override 2270 + String get messageAddLabelerDidHelper => 'Paste a labeler DID to review and subscribe to its labels.'; 2271 + 2272 + @override 2273 + String get messageAdultContentRequiredForLabels => 'Required before 18+ label preferences can be changed.'; 2274 + 2275 + @override 2276 + String get messageBuiltInLabelerActiveWhenUnavailable => 2277 + 'The built-in labeler is active even if its details cannot be loaded right now.'; 2278 + 2279 + @override 2280 + String get messageBuiltInLabelerAlwaysActive => 'This labeler is always active.'; 2281 + 2282 + @override 2283 + String get messageDevtoolsEmptyState => 'Enter a handle, DID, or AT-URI to explore\na user\'s repository.'; 2284 + 2285 + @override 2286 + String get messageEnableAdultContentForLabel => 'Enable adult content to change this 18+ label.'; 2287 + 2288 + @override 2289 + String get messageBlockedAccountDescription => 'This account is blocked'; 2290 + 2291 + @override 2292 + String get messageBlockedByAccountDescription => 'This account has blocked you'; 2293 + 2294 + @override 2295 + String get messageBlockedRelationshipDescription => 'This content is limited by a block relationship'; 2296 + 2297 + @override 2298 + String get messageHiddenContentDescription => 'This content is hidden by moderation rules'; 2299 + 2300 + @override 2301 + String get messageJsonCopiedToClipboard => 'JSON copied to clipboard'; 2302 + 2303 + @override 2304 + String get messageLogsEmpty => 'No logs yet'; 2305 + 2306 + @override 2307 + String get messageLogsEmptySubtitle => 'Log entries will appear here'; 2308 + 2309 + @override 2310 + String get messageModerationSettingsHeroSubtitle => 2311 + 'Manage adult-content visibility, subscribed labelers, and the rules each labeler applies to posts and profiles.'; 2312 + 2313 + @override 2314 + String get messageModerationGuidanceApplies => 'Moderation guidance applies here'; 2315 + 2316 + @override 2317 + String get messageMutedAccountDescription => 'Muted content is being downranked here'; 2318 + 2319 + @override 2320 + String get messageMutedPhraseDescription => 'A muted phrase matched this content'; 2321 + 2322 + @override 2323 + String get messageNoCustomLabelDefinitions => 'This labeler publishes values, but not localized custom definitions.'; 2324 + 2325 + @override 2326 + String get messageNoCustomLabelers => 'Add a labeler DID to subscribe and configure its custom labels.'; 2327 + 2328 + @override 2329 + String get messageNoLabelDescriptionAvailable => 'No description available for this label.'; 2330 + 2331 + @override 2332 + String get messageNoLogFileAvailable => 'No log file available'; 2333 + 2334 + @override 2335 + String get messageRecordCountsUnavailable => 'Record counts unavailable'; 2336 + 2337 + @override 2338 + String get messageRecordCountsLoading => 'Counting records...'; 2339 + 2340 + @override 2341 + String get messageSubscribedLabelersHeaders => 2342 + 'Subscribed labelers are added to your moderation headers and preferences.'; 2343 + 2344 + @override 2345 + String get messageUnableToOpenShareSheet => 'Unable to open share sheet. Please try again.'; 2346 + 2347 + @override 2348 + String get placeholderHandleDidOrAtUri => 'Handle, DID, or at:// URI'; 2349 + 2350 + @override 2351 + String get placeholderLabelerDid => 'did:plc:examplelabeler'; 2352 + 2353 + @override 2354 + String get placeholderLogsFilter => 'Filter logs...'; 2355 + 2356 + @override 2357 + String get subjectLazuriteLogs => 'Lazurite logs'; 2358 + 2359 + @override 2360 + String get tooltipClearAllLogs => 'Clear all logs'; 2361 + 2362 + @override 2363 + String get tooltipGoToPdsLs => 'Go to pds.ls'; 2364 + 2365 + @override 2366 + String get tooltipShareLogFile => 'Share log file'; 594 2367 }
+2592 -2
lib/core/l10n/intl_en.arb
··· 13 13 "@buttonCancel": { 14 14 "description": "Cancel button label" 15 15 }, 16 + "buttonCompose": "Compose", 17 + "@buttonCompose": { 18 + "description": "Tooltip for composing a post" 19 + }, 16 20 "buttonClearCache": "Clear Cache", 17 21 "@buttonClearCache": { 18 22 "description": "Button label to clear local cache" ··· 21 25 "@buttonContinue": { 22 26 "description": "Continue button label" 23 27 }, 28 + "buttonClearLocal": "Clear Local", 29 + "@buttonClearLocal": { 30 + "description": "Button label to clear locally stored items" 31 + }, 32 + "buttonDelete": "Delete", 33 + "@buttonDelete": { 34 + "description": "Delete button label" 35 + }, 36 + "buttonDiscard": "Discard", 37 + "@buttonDiscard": { 38 + "description": "Discard button label" 39 + }, 40 + "buttonLoadMoreQuotes": "Load more quotes", 41 + "@buttonLoadMoreQuotes": { 42 + "description": "Button label to load additional quote posts" 43 + }, 44 + "buttonLoadMoreReposts": "Load more reposts", 45 + "@buttonLoadMoreReposts": { 46 + "description": "Button label to load additional repost users" 47 + }, 24 48 "buttonRemove": "Remove", 25 49 "@buttonRemove": { 26 50 "description": "Remove button label" ··· 41 65 "@buttonOpen": { 42 66 "description": "Open button label" 43 67 }, 68 + "buttonOk": "OK", 69 + "@buttonOk": { 70 + "description": "OK button label" 71 + }, 72 + "buttonPost": "Post", 73 + "@buttonPost": { 74 + "description": "Button label to publish a post" 75 + }, 44 76 "buttonResetSignInData": "Reset Sign-In Data", 45 77 "@buttonResetSignInData": { 46 78 "description": "Button label to clear local sign-in data" ··· 49 81 "@buttonRetry": { 50 82 "description": "Retry button label" 51 83 }, 84 + "buttonSave": "Save", 85 + "@buttonSave": { 86 + "description": "Save button label" 87 + }, 88 + "buttonSaveChanges": "Save Changes", 89 + "@buttonSaveChanges": { 90 + "description": "Button label to save post edits" 91 + }, 52 92 "buttonSignIn": "Sign In", 53 93 "@buttonSignIn": { 54 94 "description": "Sign in button label" ··· 57 97 "@buttonShowContent": { 58 98 "description": "Button label to reveal moderated content" 59 99 }, 100 + "buttonShare": "Share", 101 + "@buttonShare": { 102 + "description": "Share button or menu label" 103 + }, 60 104 "buttonTryAgain": "Try again", 61 105 "@buttonTryAgain": { 62 106 "description": "Try again button label" ··· 68 112 "commonNone": "None", 69 113 "@commonNone": { 70 114 "description": "Label for no value" 115 + }, 116 + "commonNow": "now", 117 + "@commonNow": { 118 + "description": "Lowercase relative time label for the current moment" 119 + }, 120 + "commonJustNow": "Just now", 121 + "@commonJustNow": { 122 + "description": "Relative time label for the current moment" 71 123 }, 72 124 "commonNotCheckedYet": "Not checked yet", 73 125 "@commonNotCheckedYet": { ··· 77 129 "@commonOff": { 78 130 "description": "Disabled option label" 79 131 }, 132 + "commonUnknown": "unknown", 133 + "@commonUnknown": { 134 + "description": "Lowercase fallback for an unknown value" 135 + }, 80 136 "dialogClearCacheContent": "This removes cached posts, profiles, images, feeds, threads, label data, and local semantic search data.\n\nAccounts, settings, drafts, bookmarks, and likes are kept.", 81 137 "@dialogClearCacheContent": { 82 138 "description": "Confirmation dialog body before clearing local cache" ··· 85 141 "@dialogClearCacheTitle": { 86 142 "description": "Confirmation dialog title before clearing local cache" 87 143 }, 144 + "dialogClearLocalBookmarksContent": "This removes only local bookmarks from this device. Bluesky cloud bookmarks will not be deleted.", 145 + "@dialogClearLocalBookmarksContent": { 146 + "description": "Confirmation dialog body before clearing local bookmarks" 147 + }, 148 + "dialogClearLocalBookmarksTitle": "Clear local bookmarks?", 149 + "@dialogClearLocalBookmarksTitle": { 150 + "description": "Confirmation dialog title before clearing local bookmarks" 151 + }, 152 + "dialogDeletePostContent": "This action cannot be undone.", 153 + "@dialogDeletePostContent": { 154 + "description": "Confirmation dialog body before deleting a post" 155 + }, 156 + "dialogDeletePostTitle": "Delete Post?", 157 + "@dialogDeletePostTitle": { 158 + "description": "Confirmation dialog title before deleting a post" 159 + }, 160 + "dialogDeleteDraftTitle": "Delete Draft?", 161 + "@dialogDeleteDraftTitle": { 162 + "description": "Confirmation dialog title before deleting a draft" 163 + }, 164 + "dialogDiscardChangesContent": "You have unsaved edits. Discard them and leave?", 165 + "@dialogDiscardChangesContent": { 166 + "description": "Confirmation dialog body before leaving unsaved post edits" 167 + }, 168 + "dialogDiscardChangesTitle": "Discard Changes?", 169 + "@dialogDiscardChangesTitle": { 170 + "description": "Confirmation dialog title before leaving unsaved post edits" 171 + }, 172 + "dialogEditAlgorithmContent": "Lazurite saves edits by deleting and recreating the post record with the same URI. During re-indexing, ranking, counters, and search visibility can shift, and updates may take time to appear everywhere.", 173 + "@dialogEditAlgorithmContent": { 174 + "description": "Information dialog body explaining how post editing is implemented" 175 + }, 176 + "dialogEditAlgorithmTitle": "How Post Editing Works", 177 + "@dialogEditAlgorithmTitle": { 178 + "description": "Information dialog title explaining how post editing works" 179 + }, 180 + "dialogSaveDraftContent": "You have unsaved content. Would you like to save it as a draft?", 181 + "@dialogSaveDraftContent": { 182 + "description": "Confirmation dialog body before leaving compose with unsaved content" 183 + }, 184 + "dialogSaveDraftTitle": "Save Draft?", 185 + "@dialogSaveDraftTitle": { 186 + "description": "Confirmation dialog title before leaving compose with unsaved content" 187 + }, 88 188 "dialogRemoveAccountContent": "Remove @{handle} from this device?", 89 189 "@dialogRemoveAccountContent": { 90 190 "description": "Confirmation dialog body before removing a saved account from this device", ··· 139 239 "@errorGenericTitle": { 140 240 "description": "Generic error state title" 141 241 }, 242 + "errorFailedToLoadBookmarks": "Failed to load bookmarks", 243 + "@errorFailedToLoadBookmarks": { 244 + "description": "Error title shown when bookmarks cannot load" 245 + }, 246 + "errorFailedToLoadLikedPosts": "Failed to load liked posts", 247 + "@errorFailedToLoadLikedPosts": { 248 + "description": "Error title shown when liked posts cannot load" 249 + }, 250 + "errorFailedToLoadLikedPostsDetails": "Failed to load liked posts: {error}", 251 + "@errorFailedToLoadLikedPostsDetails": { 252 + "description": "Error message shown when liked posts cannot load", 253 + "placeholders": { 254 + "error": { 255 + "type": "Object", 256 + "example": "network unavailable" 257 + } 258 + } 259 + }, 260 + "errorFailedToRefreshLikedPosts": "Failed to refresh liked posts: {error}", 261 + "@errorFailedToRefreshLikedPosts": { 262 + "description": "Warning message shown when refreshing liked posts fails", 263 + "placeholders": { 264 + "error": { 265 + "type": "Object", 266 + "example": "network unavailable" 267 + } 268 + } 269 + }, 270 + "errorFailedToLoadTrending": "Failed to load trending", 271 + "@errorFailedToLoadTrending": { 272 + "description": "Error title shown when trending content cannot load" 273 + }, 274 + "errorFailedToLoadTrendingTopics": "Failed to load trending topics: {error}", 275 + "@errorFailedToLoadTrendingTopics": { 276 + "description": "Error message shown when trending topics cannot load", 277 + "placeholders": { 278 + "error": { 279 + "type": "Object", 280 + "example": "network unavailable" 281 + } 282 + } 283 + }, 284 + "errorUnknown": "Unknown error", 285 + "@errorUnknown": { 286 + "description": "Fallback message when an error has no details" 287 + }, 142 288 "errorUnableToRemoveAccount": "Unable to remove account right now.", 143 289 "@errorUnableToRemoveAccount": { 144 290 "description": "Snackbar message when a saved account cannot be removed" ··· 187 333 } 188 334 } 189 335 }, 336 + "formatLikedOn": "Liked on {date}", 337 + "@formatLikedOn": { 338 + "description": "Fallback liked-post card subtitle showing when a post was liked", 339 + "placeholders": { 340 + "date": { 341 + "type": "String", 342 + "example": "Mar 15" 343 + } 344 + } 345 + }, 346 + "formatLikesCount": "{count, plural, =1{1 Like} other{{count} Likes}}", 347 + "@formatLikesCount": { 348 + "description": "Interaction sheet tab label showing like count", 349 + "placeholders": { 350 + "count": { 351 + "type": "int", 352 + "example": "12" 353 + } 354 + } 355 + }, 356 + "formatOfflineReconnectAction": "You are offline. Reconnect to {action}.", 357 + "@formatOfflineReconnectAction": { 358 + "description": "Message explaining that a post action requires reconnecting", 359 + "placeholders": { 360 + "action": { 361 + "type": "String", 362 + "example": "like this post" 363 + } 364 + } 365 + }, 366 + "formatReplyingToHandle": "Replying to @{handle}", 367 + "@formatReplyingToHandle": { 368 + "description": "Post card reply context label showing the parent post author", 369 + "placeholders": { 370 + "handle": { 371 + "type": "String", 372 + "example": "alice.bsky.social" 373 + } 374 + } 375 + }, 376 + "formatRepostsCount": "{count, plural, =1{1 Repost} other{{count} Reposts}}", 377 + "@formatRepostsCount": { 378 + "description": "Interaction sheet tab label showing repost count", 379 + "placeholders": { 380 + "count": { 381 + "type": "int", 382 + "example": "8" 383 + } 384 + } 385 + }, 386 + "formatSavedOn": "Saved on {date}", 387 + "@formatSavedOn": { 388 + "description": "Fallback saved-post card subtitle showing when a post was saved", 389 + "placeholders": { 390 + "date": { 391 + "type": "String", 392 + "example": "Mar 15" 393 + } 394 + } 395 + }, 396 + "formatTrendingCategory": "Category: {category}", 397 + "@formatTrendingCategory": { 398 + "description": "Trending topic category subtitle line", 399 + "placeholders": { 400 + "category": { 401 + "type": "String", 402 + "example": "Technology" 403 + } 404 + } 405 + }, 406 + "formatTrendingPostCount": "{count, plural, =1{1 post} other{{count} posts}}", 407 + "@formatTrendingPostCount": { 408 + "description": "Trending topic post count subtitle line", 409 + "placeholders": { 410 + "count": { 411 + "type": "int", 412 + "example": "1200" 413 + } 414 + } 415 + }, 416 + "formatViewHandle": "View @{handle}", 417 + "@formatViewHandle": { 418 + "description": "Post overflow action label to view an author profile", 419 + "placeholders": { 420 + "handle": { 421 + "type": "String", 422 + "example": "alice.bsky.social" 423 + } 424 + } 425 + }, 426 + "formatComposeFailedToPickImage": "Failed to pick image: {error}", 427 + "@formatComposeFailedToPickImage": { 428 + "description": "Snackbar message when the image picker fails", 429 + "placeholders": { 430 + "error": { 431 + "type": "Object", 432 + "example": "permission denied" 433 + } 434 + } 435 + }, 436 + "formatComposeFailedToPickVideo": "Failed to pick video: {error}", 437 + "@formatComposeFailedToPickVideo": { 438 + "description": "Snackbar message when the video picker fails", 439 + "placeholders": { 440 + "error": { 441 + "type": "Object", 442 + "example": "permission denied" 443 + } 444 + } 445 + }, 446 + "formatComposeFailedToSaveChanges": "Failed to save changes: {error}", 447 + "@formatComposeFailedToSaveChanges": { 448 + "description": "Compose error message when saving edits fails with details", 449 + "placeholders": { 450 + "error": { 451 + "type": "Object", 452 + "example": "network unavailable" 453 + } 454 + } 455 + }, 456 + "formatComposeFailedToSubmitPost": "Failed to submit post: {error}", 457 + "@formatComposeFailedToSubmitPost": { 458 + "description": "Compose error message when post submission fails with details", 459 + "placeholders": { 460 + "error": { 461 + "type": "Object", 462 + "example": "network unavailable" 463 + } 464 + } 465 + }, 466 + "formatComposeImageTooLarge": "Image \"{fileName}\" is {sizeMb} MB - max 1 MB.", 467 + "@formatComposeImageTooLarge": { 468 + "description": "Compose validation error when an attached image is too large", 469 + "placeholders": { 470 + "fileName": { 471 + "type": "String", 472 + "example": "photo.png" 473 + }, 474 + "sizeMb": { 475 + "type": "String", 476 + "example": "1.4" 477 + } 478 + } 479 + }, 480 + "formatComposeQuotingHandle": "Quoting @{handle}", 481 + "@formatComposeQuotingHandle": { 482 + "description": "Compose quote preview label with the quoted author's handle", 483 + "placeholders": { 484 + "handle": { 485 + "type": "String", 486 + "example": "alice.bsky.social" 487 + } 488 + } 489 + }, 490 + "formatComposeScheduledFor": "Scheduled for {dateTime}", 491 + "@formatComposeScheduledFor": { 492 + "description": "Compose scheduled post pill label", 493 + "placeholders": { 494 + "dateTime": { 495 + "type": "String", 496 + "example": "May 8, 2:30 PM" 497 + } 498 + } 499 + }, 500 + "formatComposeVideoReadyWithAltText": "Ready - \"{altText}\"", 501 + "@formatComposeVideoReadyWithAltText": { 502 + "description": "Video attachment status when ready and alt text exists", 503 + "placeholders": { 504 + "altText": { 505 + "type": "String", 506 + "example": "A dog running through a park" 507 + } 508 + } 509 + }, 510 + "formatComposeVideoTooLarge": "Video is {sizeMb} MB - exceeds the 100 MB limit.", 511 + "@formatComposeVideoTooLarge": { 512 + "description": "Compose validation error when a video is too large", 513 + "placeholders": { 514 + "sizeMb": { 515 + "type": "String", 516 + "example": "125.4" 517 + } 518 + } 519 + }, 520 + "formatDraftCount": "{count, plural, =1{1 draft} other{{count} drafts}}", 521 + "@formatDraftCount": { 522 + "description": "Compose drafts panel count label", 523 + "placeholders": { 524 + "count": { 525 + "type": "int", 526 + "example": "3" 527 + } 528 + } 529 + }, 530 + "actionLikeThisPost": "like this post", 531 + "@actionLikeThisPost": { 532 + "description": "Offline action phrase for liking a post" 533 + }, 534 + "actionReplyToThisPost": "reply to this post", 535 + "@actionReplyToThisPost": { 536 + "description": "Offline action phrase for replying to a post" 537 + }, 538 + "actionRepostThisPost": "repost this post", 539 + "@actionRepostThisPost": { 540 + "description": "Offline action phrase for reposting a post" 541 + }, 542 + "actionPublishYourPost": "publish your post", 543 + "@actionPublishYourPost": { 544 + "description": "Offline action phrase for publishing a post" 545 + }, 190 546 "labelAbout": "About", 191 547 "@labelAbout": { 192 548 "description": "About page or settings label" ··· 255 611 "@labelBookmarksAndLikes": { 256 612 "description": "Settings item for bookmarked and liked posts" 257 613 }, 614 + "labelBookmarkActions": "Bookmark actions", 615 + "@labelBookmarkActions": { 616 + "description": "Tooltip for bookmark actions menu" 617 + }, 618 + "labelBookmarkedPost": "Bookmarked Post", 619 + "@labelBookmarkedPost": { 620 + "description": "Fallback saved-post card title" 621 + }, 622 + "labelBookmarks": "Bookmarks", 623 + "@labelBookmarks": { 624 + "description": "Bookmarks tab label" 625 + }, 626 + "labelBluesky": "Bluesky", 627 + "@labelBluesky": { 628 + "description": "Bluesky source tab label" 629 + }, 630 + "labelAlt": "ALT", 631 + "@labelAlt": { 632 + "description": "Short all-caps label for media alt text controls" 633 + }, 258 634 "labelCacheCleared": "Cache cleared", 259 635 "@labelCacheCleared": { 260 636 "description": "Snackbar message after clearing cache" ··· 267 643 "@labelCleanFollows": { 268 644 "description": "Settings item for follow audit feature" 269 645 }, 646 + "labelClose": "Close", 647 + "@labelClose": { 648 + "description": "Close button tooltip" 649 + }, 270 650 "labelClearCache": "Clear Cache", 271 651 "@labelClearCache": { 272 652 "description": "Settings item for clearing cache" ··· 395 775 "@labelNewPost": { 396 776 "description": "New post menu label" 397 777 }, 778 + "labelNow": "NOW", 779 + "@labelNow": { 780 + "description": "Uppercase compact relative time label for the current moment" 781 + }, 398 782 "labelNotifications": "Notifications", 399 783 "@labelNotifications": { 400 784 "description": "Notifications menu label" ··· 515 899 "@labelClearUntil": { 516 900 "description": "Button label to clear until date filter" 517 901 }, 902 + "labelClearLocalBookmarks": "Clear local bookmarks", 903 + "@labelClearLocalBookmarks": { 904 + "description": "Bookmark menu item to clear local bookmarks" 905 + }, 906 + "labelCopyLink": "Copy Link", 907 + "@labelCopyLink": { 908 + "description": "Post overflow menu item to copy a post link" 909 + }, 910 + "labelDeletePost": "Delete Post", 911 + "@labelDeletePost": { 912 + "description": "Post overflow menu item to delete a post" 913 + }, 914 + "labelDeleteDraft": "Delete draft", 915 + "@labelDeleteDraft": { 916 + "description": "Tooltip for deleting a draft" 917 + }, 918 + "labelEditPost": "Edit Post", 919 + "@labelEditPost": { 920 + "description": "Post overflow menu item to edit a post" 921 + }, 922 + "labelLiked": "Liked", 923 + "@labelLiked": { 924 + "description": "Liked posts tab label" 925 + }, 926 + "labelLikedBy": "LIKED BY", 927 + "@labelLikedBy": { 928 + "description": "Post interactions sheet section label for users who liked a post" 929 + }, 930 + "labelLikedPost": "Liked Post", 931 + "@labelLikedPost": { 932 + "description": "Fallback liked-post card title" 933 + }, 934 + "labelMoreInfo": "More info", 935 + "@labelMoreInfo": { 936 + "description": "Tooltip for opening more information" 937 + }, 938 + "labelLocal": "Local", 939 + "@labelLocal": { 940 + "description": "Local saved-post source tab label" 941 + }, 942 + "labelOpenPost": "Open post", 943 + "@labelOpenPost": { 944 + "description": "Tooltip to open a post" 945 + }, 946 + "labelQuotePost": "Quote Post", 947 + "@labelQuotePost": { 948 + "description": "Post action label to quote a post" 949 + }, 950 + "labelQuoteReposts": "QUOTE / REPOSTS", 951 + "@labelQuoteReposts": { 952 + "description": "Heading for quote and repost bottom sheet" 953 + }, 954 + "labelQuotes": "Quotes", 955 + "@labelQuotes": { 956 + "description": "Quotes section title" 957 + }, 958 + "labelRemoveFromBluesky": "Remove from Bluesky", 959 + "@labelRemoveFromBluesky": { 960 + "description": "Post save menu item to remove a cloud bookmark" 961 + }, 962 + "labelRemoveLocalSave": "Remove local save", 963 + "@labelRemoveLocalSave": { 964 + "description": "Post save menu item to remove a local bookmark" 965 + }, 966 + "labelReportPost": "Report Post", 967 + "@labelReportPost": { 968 + "description": "Post overflow menu item to report a post" 969 + }, 970 + "labelRepost": "Repost", 971 + "@labelRepost": { 972 + "description": "Post action label to repost a post" 973 + }, 974 + "labelRepostedBy": "REPOSTED BY", 975 + "@labelRepostedBy": { 976 + "description": "Post interactions sheet section label for users who reposted a post" 977 + }, 978 + "labelReposts": "Reposts", 979 + "@labelReposts": { 980 + "description": "Reposts section title" 981 + }, 982 + "labelSaveImage": "Save image", 983 + "@labelSaveImage": { 984 + "description": "Image context menu item to save an image" 985 + }, 986 + "labelSaveLocally": "Save locally", 987 + "@labelSaveLocally": { 988 + "description": "Post save menu item to create a local bookmark" 989 + }, 990 + "labelSaveToBluesky": "Save to Bluesky", 991 + "@labelSaveToBluesky": { 992 + "description": "Post save menu item to create a cloud bookmark" 993 + }, 994 + "labelSchedule": "Schedule", 995 + "@labelSchedule": { 996 + "description": "Compose toolbar schedule button tooltip" 997 + }, 998 + "labelScheduled": "Scheduled", 999 + "@labelScheduled": { 1000 + "description": "Scheduled draft badge label" 1001 + }, 518 1002 "labelSavedAccounts": "Saved accounts", 519 1003 "@labelSavedAccounts": { 520 1004 "description": "Saved accounts section label on login screen" ··· 527 1011 "@labelSemanticSearch": { 528 1012 "description": "Semantic search settings label" 529 1013 }, 1014 + "labelShowLikedUsers": "Show Liked Users", 1015 + "@labelShowLikedUsers": { 1016 + "description": "Post overflow menu item to open users who liked a post" 1017 + }, 1018 + "labelShowQuoteRepostList": "Show Quote/Repost List", 1019 + "@labelShowQuoteRepostList": { 1020 + "description": "Post overflow menu item to open quote and repost lists" 1021 + }, 530 1022 "labelSettings": "Settings", 531 1023 "@labelSettings": { 532 1024 "description": "Settings page title or tooltip" ··· 566 1058 "labelTroubleshooting": "Troubleshooting", 567 1059 "@labelTroubleshooting": { 568 1060 "description": "Troubleshooting settings section label" 1061 + }, 1062 + "labelVideo": "Video", 1063 + "@labelVideo": { 1064 + "description": "Fallback video label" 1065 + }, 1066 + "labelTopics": "Topics", 1067 + "@labelTopics": { 1068 + "description": "Trending topics section label" 1069 + }, 1070 + "labelTrending": "Trending", 1071 + "@labelTrending": { 1072 + "description": "Trending screen title and tooltip label" 1073 + }, 1074 + "labelSuggested": "Suggested", 1075 + "@labelSuggested": { 1076 + "description": "Trending suggested topics section label" 1077 + }, 1078 + "labelSuggestedFollows": "Suggested Follows", 1079 + "@labelSuggestedFollows": { 1080 + "description": "Suggested follows sheet and menu label" 1081 + }, 1082 + "labelUnrepost": "Unrepost", 1083 + "@labelUnrepost": { 1084 + "description": "Post action label to undo a repost" 569 1085 }, 570 1086 "labelTypeaheadProvider": "Typeahead Provider", 571 1087 "@labelTypeaheadProvider": { ··· 655 1171 "@messageFeedsSubtitle": { 656 1172 "description": "Settings subtitle for feeds" 657 1173 }, 1174 + "messageLinkCopiedToClipboard": "Link copied to clipboard", 1175 + "@messageLinkCopiedToClipboard": { 1176 + "description": "Snackbar message after copying a post link" 1177 + }, 1178 + "messageLoadingTrendingTopics": "Loading trending topics", 1179 + "@messageLoadingTrendingTopics": { 1180 + "description": "Loading message while trending topics load" 1181 + }, 658 1182 "messageForceNextXrpc401Subtitle": "Debug-only: next network request returns Unauthorized to test token refresh", 659 1183 "@messageForceNextXrpc401Subtitle": { 660 1184 "description": "Developer setting subtitle for forced 401" ··· 663 1187 "@messageManageSemanticSearchSubtitle": { 664 1188 "description": "Settings subtitle for semantic search management" 665 1189 }, 666 - 1190 + "messageMetadataTemporarilyUnavailable": "Metadata temporarily unavailable", 1191 + "@messageMetadataTemporarilyUnavailable": { 1192 + "description": "Trending banner shown when supplemental metadata cannot be loaded" 1193 + }, 667 1194 "messageModeratedContentCannotReveal": "Hidden by your moderation settings and cannot be revealed here.", 668 1195 "@messageModeratedContentCannotReveal": { 669 1196 "description": "Moderation overlay description when content cannot be revealed" 670 1197 }, 671 - 672 1198 "messageModeratedContentCanReveal": "Hidden by your moderation settings. You can reveal it for this view.", 673 1199 "@messageModeratedContentCanReveal": { 674 1200 "description": "Moderation overlay description when content can be revealed" ··· 677 1203 "@messageProviderDiagnosticsSubtitle": { 678 1204 "description": "Settings subtitle for provider diagnostics" 679 1205 }, 1206 + "messageLikedPostsUnavailable": "Liked posts are unavailable right now.", 1207 + "@messageLikedPostsUnavailable": { 1208 + "description": "Message shown when liked posts dependencies are unavailable" 1209 + }, 1210 + "messageChangesSaved": "Changes saved.", 1211 + "@messageChangesSaved": { 1212 + "description": "Snackbar message after post edits are saved" 1213 + }, 1214 + "messageComposeAddAltText": "Add alt text", 1215 + "@messageComposeAddAltText": { 1216 + "description": "Tooltip for adding video alt text" 1217 + }, 1218 + "messageComposeAddImage": "Add image", 1219 + "@messageComposeAddImage": { 1220 + "description": "Compose toolbar add image tooltip" 1221 + }, 1222 + "messageComposeAddVideo": "Add video", 1223 + "@messageComposeAddVideo": { 1224 + "description": "Compose toolbar add video tooltip" 1225 + }, 1226 + "messageComposeClearScheduledTime": "Clear scheduled time", 1227 + "@messageComposeClearScheduledTime": { 1228 + "description": "Tooltip for clearing scheduled compose time" 1229 + }, 1230 + "messageComposeDescribeImage": "Describe the image", 1231 + "@messageComposeDescribeImage": { 1232 + "description": "Image alt text field placeholder" 1233 + }, 1234 + "messageComposeDescribeVideo": "Describe the video", 1235 + "@messageComposeDescribeVideo": { 1236 + "description": "Video alt text field placeholder" 1237 + }, 1238 + "messageComposeDraftSaved": "Draft saved", 1239 + "@messageComposeDraftSaved": { 1240 + "description": "Snackbar message after saving a draft" 1241 + }, 1242 + "messageComposeDrafts": "Drafts", 1243 + "@messageComposeDrafts": { 1244 + "description": "Compose drafts panel title and toolbar tooltip" 1245 + }, 1246 + "messageComposeEditNotice": "Edits are saved by replacing the record while keeping this post URI. Ranking, counts, and visibility may shift while networks re-index.", 1247 + "@messageComposeEditNotice": { 1248 + "description": "Compose edit mode notice banner" 1249 + }, 1250 + "messageComposeImageAltTextTitle": "Alt text", 1251 + "@messageComposeImageAltTextTitle": { 1252 + "description": "Image alt text dialog title" 1253 + }, 1254 + "messageComposeImageMaxCount": "Maximum 4 images allowed", 1255 + "@messageComposeImageMaxCount": { 1256 + "description": "Compose validation message when too many images are attached" 1257 + }, 1258 + "messageComposeImageMustBeJpegPngWebp": "Image must be JPEG, PNG, or WebP", 1259 + "@messageComposeImageMustBeJpegPngWebp": { 1260 + "description": "Compose validation message for unsupported image extensions" 1261 + }, 1262 + "messageComposeImageMustBeUnder1Mb": "Image must be smaller than 1MB", 1263 + "@messageComposeImageMustBeUnder1Mb": { 1264 + "description": "Compose validation message for image picker size validation" 1265 + }, 1266 + "messageComposeNoDraftsSaved": "No drafts saved", 1267 + "@messageComposeNoDraftsSaved": { 1268 + "description": "Empty state text in compose drafts panel" 1269 + }, 1270 + "messageComposeNoText": "(No text)", 1271 + "@messageComposeNoText": { 1272 + "description": "Fallback draft content label when a draft has no text" 1273 + }, 1274 + "messageComposePlaceholder": "What''s on your mind?", 1275 + "@messageComposePlaceholder": { 1276 + "description": "Compose text field placeholder" 1277 + }, 1278 + "messageComposePreviewUnavailable": "Preview unavailable", 1279 + "@messageComposePreviewUnavailable": { 1280 + "description": "Video alt text preview unavailable message" 1281 + }, 1282 + "messageComposeQuotingPost": "Quoting post", 1283 + "@messageComposeQuotingPost": { 1284 + "description": "Compose quote preview label without an author handle" 1285 + }, 1286 + "messageComposeRemoveExistingMediaBeforeVideo": "Remove existing media before adding a video", 1287 + "@messageComposeRemoveExistingMediaBeforeVideo": { 1288 + "description": "Compose validation message when a video cannot be added because other media exists" 1289 + }, 1290 + "messageComposeRemoveImage": "Remove image", 1291 + "@messageComposeRemoveImage": { 1292 + "description": "Tooltip for removing an image attachment" 1293 + }, 1294 + "messageComposeRemoveQuotedPost": "Remove quoted post", 1295 + "@messageComposeRemoveQuotedPost": { 1296 + "description": "Tooltip for removing a quoted post from compose" 1297 + }, 1298 + "messageComposeSaveDraft": "Save draft", 1299 + "@messageComposeSaveDraft": { 1300 + "description": "Compose toolbar save draft tooltip" 1301 + }, 1302 + "messageComposeVideoAltTextTitle": "Video alt text", 1303 + "@messageComposeVideoAltTextTitle": { 1304 + "description": "Video alt text dialog title" 1305 + }, 1306 + "messageVideoCheckingUploadLimits": "Checking upload limits...", 1307 + "@messageVideoCheckingUploadLimits": { 1308 + "description": "Video attachment status while checking upload limits" 1309 + }, 1310 + "messageVideoDailyUploadLimitReached": "Daily video upload limit reached.", 1311 + "@messageVideoDailyUploadLimitReached": { 1312 + "description": "Video upload validation message when the daily limit is reached" 1313 + }, 1314 + "messageVideoProcessing": "Processing...", 1315 + "@messageVideoProcessing": { 1316 + "description": "Video attachment processing status" 1317 + }, 1318 + "messageVideoProcessingFailed": "Video processing failed.", 1319 + "@messageVideoProcessingFailed": { 1320 + "description": "Video attachment error when processing fails" 1321 + }, 1322 + "messageVideoProcessingTimedOut": "Video processing timed out.", 1323 + "@messageVideoProcessingTimedOut": { 1324 + "description": "Video attachment error when processing times out" 1325 + }, 1326 + "messageVideoReady": "Ready", 1327 + "@messageVideoReady": { 1328 + "description": "Video attachment ready status" 1329 + }, 1330 + "messageVideoReadyToUpload": "Ready to upload", 1331 + "@messageVideoReadyToUpload": { 1332 + "description": "Video attachment ready-to-upload status" 1333 + }, 1334 + "messageVideoUploadFailed": "Upload failed - please try again.", 1335 + "@messageVideoUploadFailed": { 1336 + "description": "Video upload generic failure message" 1337 + }, 1338 + "messageVideoUploading": "Uploading...", 1339 + "@messageVideoUploading": { 1340 + "description": "Video attachment uploading status" 1341 + }, 1342 + "errorComposeChangedElsewhere": "This post was changed elsewhere. Reopen it and try editing again.", 1343 + "@errorComposeChangedElsewhere": { 1344 + "description": "Compose edit error when the post changed remotely" 1345 + }, 1346 + "errorComposeCouldNotConfirmEdit": "Edit was submitted but could not be confirmed yet. Please reopen the post and verify.", 1347 + "@errorComposeCouldNotConfirmEdit": { 1348 + "description": "Compose edit error when an edit cannot be confirmed" 1349 + }, 1350 + "errorComposeCouldNotSaveAndConfirmRecovery": "Could not save changes and we could not confirm recovery. Reopen the thread and verify the post.", 1351 + "@errorComposeCouldNotSaveAndConfirmRecovery": { 1352 + "description": "Compose edit recovery error when save and recovery status are unknown" 1353 + }, 1354 + "errorComposeEditContextMissing": "Edit context is missing. Please reopen the editor and try again.", 1355 + "@errorComposeEditContextMissing": { 1356 + "description": "Compose edit validation error when edit context is missing" 1357 + }, 1358 + "errorComposeFailedToCreatePost": "Failed to create post. Please try again.", 1359 + "@errorComposeFailedToCreatePost": { 1360 + "description": "Compose error when creating a post fails" 1361 + }, 1362 + "errorComposeFailedToSaveChanges": "Failed to save changes. Please try again.", 1363 + "@errorComposeFailedToSaveChanges": { 1364 + "description": "Compose error when saving post edits fails" 1365 + }, 1366 + "errorComposeFailedToUploadImage": "Failed to upload image. Please try again.", 1367 + "@errorComposeFailedToUploadImage": { 1368 + "description": "Compose error when image upload fails" 1369 + }, 1370 + "errorComposeImageFileNotFound": "Image file not found. Please re-attach and try again.", 1371 + "@errorComposeImageFileNotFound": { 1372 + "description": "Compose error when an attached image file is missing" 1373 + }, 1374 + "errorComposeNetworkSavedAsDraft": "Network error - post saved as draft.", 1375 + "@errorComposeNetworkSavedAsDraft": { 1376 + "description": "Compose error when submission fails and is saved as a draft" 1377 + }, 1378 + "errorComposeOriginalPostRestored": "Could not save changes. Your original post was restored.", 1379 + "@errorComposeOriginalPostRestored": { 1380 + "description": "Compose edit error when the original post was restored after failure" 1381 + }, 1382 + "errorComposeUnsupportedImageFormat": "Unsupported image format. Use JPEG, PNG, or WebP.", 1383 + "@errorComposeUnsupportedImageFormat": { 1384 + "description": "Compose validation error for unsupported image bytes" 1385 + }, 1386 + "messageNoBookmarks": "No bookmarks", 1387 + "@messageNoBookmarks": { 1388 + "description": "Empty state title when there are no bookmarks" 1389 + }, 1390 + "messageNoBookmarksSubtitle": "Posts you bookmark will appear here", 1391 + "@messageNoBookmarksSubtitle": { 1392 + "description": "Empty state subtitle when there are no bookmarks" 1393 + }, 1394 + "messageNoBookmarksInSource": "No bookmarks in this source", 1395 + "@messageNoBookmarksInSource": { 1396 + "description": "Empty state title when a bookmark source tab is empty" 1397 + }, 1398 + "messageNoBookmarksInSourceSubtitle": "Try switching tabs or saving posts to this source", 1399 + "@messageNoBookmarksInSourceSubtitle": { 1400 + "description": "Empty state subtitle when a bookmark source tab is empty" 1401 + }, 1402 + "messageNoInteractionsYet": "No interactions yet", 1403 + "@messageNoInteractionsYet": { 1404 + "description": "Empty state message when a post has no likes or reposts" 1405 + }, 1406 + "messageNoLikedPosts": "No liked posts", 1407 + "@messageNoLikedPosts": { 1408 + "description": "Empty state title when there are no liked posts" 1409 + }, 1410 + "messageNoLikedPostsSubtitle": "Posts you like will appear here after sync", 1411 + "@messageNoLikedPostsSubtitle": { 1412 + "description": "Empty state subtitle when there are no liked posts" 1413 + }, 1414 + "messageNoLikedPostsYet": "No liked posts yet", 1415 + "@messageNoLikedPostsYet": { 1416 + "description": "Empty state when a profile has no liked posts" 1417 + }, 1418 + "messageNoQuotesYet": "No quotes yet", 1419 + "@messageNoQuotesYet": { 1420 + "description": "Empty state message when a post has no quote posts" 1421 + }, 1422 + "messageNoRepostsYet": "No reposts yet", 1423 + "@messageNoRepostsYet": { 1424 + "description": "Empty state message when a post has no reposts" 1425 + }, 1426 + "messageNoTrendingTopicsRightNow": "No trending topics right now", 1427 + "@messageNoTrendingTopicsRightNow": { 1428 + "description": "Empty state message when there are no trending topics" 1429 + }, 1430 + "messagePostDeleted": "Post deleted", 1431 + "@messagePostDeleted": { 1432 + "description": "Snackbar message after deleting a post" 1433 + }, 1434 + "messageQuotePostSubtitle": "Quote this post with your own text", 1435 + "@messageQuotePostSubtitle": { 1436 + "description": "Post action subtitle for quote post" 1437 + }, 1438 + "messageQuotedPostBlocked": "Quoted post is blocked", 1439 + "@messageQuotedPostBlocked": { 1440 + "description": "Quoted embed unavailable message for blocked quoted posts" 1441 + }, 1442 + "messageQuotedPostNotFound": "Quoted post not found", 1443 + "@messageQuotedPostNotFound": { 1444 + "description": "Quoted embed unavailable message for missing quoted posts" 1445 + }, 1446 + "messageQuotedPostUnavailable": "Quoted post is unavailable", 1447 + "@messageQuotedPostUnavailable": { 1448 + "description": "Quoted embed unavailable message for detached or unavailable quoted posts" 1449 + }, 1450 + "messageRemoveRepostSubtitle": "Remove this repost", 1451 + "@messageRemoveRepostSubtitle": { 1452 + "description": "Post action subtitle for removing a repost" 1453 + }, 1454 + "messageReplyInThread": "Reply in a thread", 1455 + "@messageReplyInThread": { 1456 + "description": "Post card reply context label when parent post details are unavailable" 1457 + }, 1458 + "messageReplyingTo": "Replying to", 1459 + "@messageReplyingTo": { 1460 + "description": "Compose reply banner prefix before the replied-to handle" 1461 + }, 1462 + "messageShareThisPostSubtitle": "Share this post", 1463 + "@messageShareThisPostSubtitle": { 1464 + "description": "Post action subtitle for reposting a post" 1465 + }, 1466 + "messageShowLikedUsersSubtitle": "View who liked this post", 1467 + "@messageShowLikedUsersSubtitle": { 1468 + "description": "Post overflow subtitle for viewing users who liked a post" 1469 + }, 1470 + "messageShowQuoteRepostListSubtitle": "View quote posts and expand reposts", 1471 + "@messageShowQuoteRepostListSubtitle": { 1472 + "description": "Post overflow subtitle for viewing quotes and reposts" 1473 + }, 680 1474 "messageRefreshProviderHealthSubtitle": "Probe public AppView endpoints now", 681 1475 "@messageRefreshProviderHealthSubtitle": { 682 1476 "description": "Settings subtitle for refreshing provider health" ··· 733 1527 "@messageSearchPeoplePlaceholder": { 734 1528 "description": "Search people field placeholder" 735 1529 }, 1530 + "messageSearchForPeoplePlaceholder": "Search for people", 1531 + "@messageSearchForPeoplePlaceholder": { 1532 + "description": "Search field placeholder when adding list members" 1533 + }, 736 1534 "messageSearchFeedsPlaceholder": "Search feeds", 737 1535 "@messageSearchFeedsPlaceholder": { 738 1536 "description": "Search feeds field placeholder" ··· 769 1567 "@promptHandleOrDid": { 770 1568 "description": "Label for handle or DID sign-in field" 771 1569 }, 1570 + "buttonAddFeed": "Add feed", 1571 + "@buttonAddFeed": { 1572 + "description": "Button label to add a feed" 1573 + }, 1574 + "buttonAddMembers": "Add members", 1575 + "@buttonAddMembers": { 1576 + "description": "Button label to add members to a list" 1577 + }, 1578 + "buttonBlock": "Block", 1579 + "@buttonBlock": { 1580 + "description": "Button label to block an account" 1581 + }, 1582 + "buttonCreate": "Create", 1583 + "@buttonCreate": { 1584 + "description": "Create button label" 1585 + }, 1586 + "buttonEdit": "Edit", 1587 + "@buttonEdit": { 1588 + "description": "Edit button label" 1589 + }, 1590 + "buttonFollow": "Follow", 1591 + "@buttonFollow": { 1592 + "description": "Button label to follow an account" 1593 + }, 1594 + "buttonFollowAll": "Follow all", 1595 + "@buttonFollowAll": { 1596 + "description": "Button label to follow all starter pack members" 1597 + }, 1598 + "buttonFollowing": "Following", 1599 + "@buttonFollowing": { 1600 + "description": "Button label showing that an account is followed" 1601 + }, 1602 + "buttonFollowingInProgress": "Following…", 1603 + "@buttonFollowingInProgress": { 1604 + "description": "Button label while following all starter pack members" 1605 + }, 1606 + "buttonLoadMore": "Load more", 1607 + "@buttonLoadMore": { 1608 + "description": "Button label to load more results" 1609 + }, 1610 + "buttonMute": "Mute", 1611 + "@buttonMute": { 1612 + "description": "Button label to mute an account or list" 1613 + }, 1614 + "buttonScan": "Scan", 1615 + "@buttonScan": { 1616 + "description": "Button label to start a follow audit scan" 1617 + }, 1618 + "buttonSeeAll": "See all", 1619 + "@buttonSeeAll": { 1620 + "description": "Button label to view all items" 1621 + }, 1622 + "buttonShowAccounts": "Show accounts", 1623 + "@buttonShowAccounts": { 1624 + "description": "Button label to reveal blocked-by accounts" 1625 + }, 1626 + "buttonSubmitReport": "Submit Report", 1627 + "@buttonSubmitReport": { 1628 + "description": "Button label to submit a moderation report" 1629 + }, 1630 + "buttonUnblock": "Unblock", 1631 + "@buttonUnblock": { 1632 + "description": "Button label to unblock an account" 1633 + }, 1634 + "buttonUnfollow": "Unfollow", 1635 + "@buttonUnfollow": { 1636 + "description": "Button label to unfollow an account" 1637 + }, 1638 + "buttonUnfollowSelected": "Unfollow Selected ({count})", 1639 + "@buttonUnfollowSelected": { 1640 + "description": "Button label to unfollow selected audit results", 1641 + "placeholders": { 1642 + "count": { 1643 + "type": "int", 1644 + "example": "3" 1645 + } 1646 + } 1647 + }, 1648 + "buttonUnmute": "Unmute", 1649 + "@buttonUnmute": { 1650 + "description": "Button label to unmute an account or list" 1651 + }, 1652 + "dialogBlockAccountContent": "They will not be able to see your posts or interact with you. They will not be notified that you blocked them.", 1653 + "@dialogBlockAccountContent": { 1654 + "description": "Confirmation dialog body before blocking an account" 1655 + }, 1656 + "dialogBlockAccountTitle": "Block Account?", 1657 + "@dialogBlockAccountTitle": { 1658 + "description": "Confirmation dialog title before blocking an account" 1659 + }, 1660 + "dialogDeleteListTitle": "Delete list?", 1661 + "@dialogDeleteListTitle": { 1662 + "description": "Confirmation dialog title before deleting a list" 1663 + }, 1664 + "dialogDeleteStarterPackContent": "This will permanently delete this starter pack and its backing list. This cannot be undone.", 1665 + "@dialogDeleteStarterPackContent": { 1666 + "description": "Confirmation dialog body before deleting a starter pack" 1667 + }, 1668 + "dialogDeleteStarterPackTitle": "Delete starter pack", 1669 + "@dialogDeleteStarterPackTitle": { 1670 + "description": "Confirmation dialog title before deleting a starter pack" 1671 + }, 1672 + "dialogMuteAccountContent": "You will no longer see their posts or receive notifications from them.", 1673 + "@dialogMuteAccountContent": { 1674 + "description": "Confirmation dialog body before muting an account" 1675 + }, 1676 + "dialogMuteAccountTitle": "Mute Account?", 1677 + "@dialogMuteAccountTitle": { 1678 + "description": "Confirmation dialog title before muting an account" 1679 + }, 1680 + "dialogUnblockAccountContent": "They will be able to see your posts and interact with you again.", 1681 + "@dialogUnblockAccountContent": { 1682 + "description": "Confirmation dialog body before unblocking an account" 1683 + }, 1684 + "dialogUnblockAccountTitle": "Unblock Account?", 1685 + "@dialogUnblockAccountTitle": { 1686 + "description": "Confirmation dialog title before unblocking an account" 1687 + }, 1688 + "dialogUnfollowAccountContent": "You will no longer see their posts in your feed.", 1689 + "@dialogUnfollowAccountContent": { 1690 + "description": "Confirmation dialog body before unfollowing an account" 1691 + }, 1692 + "dialogUnfollowAccountTitle": "Unfollow?", 1693 + "@dialogUnfollowAccountTitle": { 1694 + "description": "Confirmation dialog title before unfollowing an account" 1695 + }, 1696 + "dialogUnmuteAccountContent": "You will see their posts and receive notifications again.", 1697 + "@dialogUnmuteAccountContent": { 1698 + "description": "Confirmation dialog body before unmuting an account" 1699 + }, 1700 + "dialogUnmuteAccountTitle": "Unmute Account?", 1701 + "@dialogUnmuteAccountTitle": { 1702 + "description": "Confirmation dialog title before unmuting an account" 1703 + }, 1704 + "errorFailedToCreateStarterPack": "Failed to create starter pack", 1705 + "@errorFailedToCreateStarterPack": { 1706 + "description": "Error message when starter pack creation fails" 1707 + }, 1708 + "errorFailedToLoadAccounts": "Failed to load accounts", 1709 + "@errorFailedToLoadAccounts": { 1710 + "description": "Error message when accounts cannot load" 1711 + }, 1712 + "errorFailedToLoadFeed": "Failed to load feed", 1713 + "@errorFailedToLoadFeed": { 1714 + "description": "Error message when a list feed cannot load" 1715 + }, 1716 + "errorFailedToLoadFeeds": "Failed to load feeds", 1717 + "@errorFailedToLoadFeeds": { 1718 + "description": "Error message when feed picker suggestions cannot load" 1719 + }, 1720 + "errorFailedToLoadList": "Failed to load list", 1721 + "@errorFailedToLoadList": { 1722 + "description": "Error message when a list cannot load" 1723 + }, 1724 + "errorFailedToLoadLists": "Failed to load lists", 1725 + "@errorFailedToLoadLists": { 1726 + "description": "Error message when lists cannot load" 1727 + }, 1728 + "errorFailedToLoadMembers": "Failed to load members", 1729 + "@errorFailedToLoadMembers": { 1730 + "description": "Error message when list members cannot load" 1731 + }, 1732 + "errorFailedToLoadMore": "Failed to load more", 1733 + "@errorFailedToLoadMore": { 1734 + "description": "Error message when loading more results fails" 1735 + }, 1736 + "errorFailedToLoadPosts": "Failed to load posts", 1737 + "@errorFailedToLoadPosts": { 1738 + "description": "Error message when posts cannot load" 1739 + }, 1740 + "errorFailedToLoadProfile": "Unable to load profile", 1741 + "@errorFailedToLoadProfile": { 1742 + "description": "Error title when a profile cannot load" 1743 + }, 1744 + "errorFailedToLoadStarterPack": "Failed to load starter pack", 1745 + "@errorFailedToLoadStarterPack": { 1746 + "description": "Error message when a starter pack cannot load" 1747 + }, 1748 + "errorFailedToLoadStarterPacks": "Failed to load starter packs", 1749 + "@errorFailedToLoadStarterPacks": { 1750 + "description": "Error message when starter packs cannot load" 1751 + }, 1752 + "errorFailedToLoadSuggestions": "Failed to load suggestions", 1753 + "@errorFailedToLoadSuggestions": { 1754 + "description": "Error title when suggested follows cannot load" 1755 + }, 1756 + "errorFollowAuditFailed": "Failed to complete follow audit.", 1757 + "@errorFollowAuditFailed": { 1758 + "description": "Error message when follow audit fails" 1759 + }, 1760 + "errorImageTooLarge": "Image must be smaller than 1MB", 1761 + "@errorImageTooLarge": { 1762 + "description": "Validation error when a profile image is too large" 1763 + }, 1764 + "errorInvalidProfileImageType": "Use a JPEG or PNG image", 1765 + "@errorInvalidProfileImageType": { 1766 + "description": "Validation error when profile image file type is unsupported" 1767 + }, 1768 + "errorProfileImageReadFailed": "Unable to read selected image", 1769 + "@errorProfileImageReadFailed": { 1770 + "description": "Error when a selected profile image cannot be read" 1771 + }, 1772 + "errorReportFailed": "Unable to submit your report. Please try again later.", 1773 + "@errorReportFailed": { 1774 + "description": "Report submission failure dialog body" 1775 + }, 1776 + "errorReportFailedTitle": "Report Failed", 1777 + "@errorReportFailedTitle": { 1778 + "description": "Report submission failure dialog title" 1779 + }, 1780 + "errorUnableToLoadConnections": "Unable to load {tab}", 1781 + "@errorUnableToLoadConnections": { 1782 + "description": "Error title when a profile connection tab cannot load", 1783 + "placeholders": { 1784 + "tab": { 1785 + "type": "String", 1786 + "example": "following" 1787 + } 1788 + } 1789 + }, 1790 + "errorUnableToUpdateProfile": "Unable to update profile", 1791 + "@errorUnableToUpdateProfile": { 1792 + "description": "Snackbar message when profile update fails" 1793 + }, 1794 + "formatAccountCount": "{count, plural, =1{1 account} other{{count} accounts}}", 1795 + "@formatAccountCount": { 1796 + "description": "Count of accounts", 1797 + "placeholders": { 1798 + "count": { 1799 + "type": "int", 1800 + "example": "4" 1801 + } 1802 + } 1803 + }, 1804 + "formatBlockedByAccountsUnavailable": "Found {count, plural, =1{1 blocked-by account} other{{count} blocked-by accounts}}, but public Bluesky profile details could not be loaded.", 1805 + "@formatBlockedByAccountsUnavailable": { 1806 + "description": "Message when blocked-by account count exists but profiles are unavailable", 1807 + "placeholders": { 1808 + "count": { 1809 + "type": "int", 1810 + "example": "12" 1811 + } 1812 + } 1813 + }, 1814 + "formatConnectionsLoading": "Loading {tab}...", 1815 + "@formatConnectionsLoading": { 1816 + "description": "Loading message for a profile connection tab", 1817 + "placeholders": { 1818 + "tab": { 1819 + "type": "String", 1820 + "example": "followers" 1821 + } 1822 + } 1823 + }, 1824 + "formatConnectionsNoMatches": "No {tab} match \"{query}\"", 1825 + "@formatConnectionsNoMatches": { 1826 + "description": "Empty search message for a profile connection tab", 1827 + "placeholders": { 1828 + "tab": { 1829 + "type": "String", 1830 + "example": "followers" 1831 + }, 1832 + "query": { 1833 + "type": "String", 1834 + "example": "alice" 1835 + } 1836 + } 1837 + }, 1838 + "formatConnectionsNoneFound": "No {tab} found", 1839 + "@formatConnectionsNoneFound": { 1840 + "description": "Empty message for a profile connection tab", 1841 + "placeholders": { 1842 + "tab": { 1843 + "type": "String", 1844 + "example": "followers" 1845 + } 1846 + } 1847 + }, 1848 + "formatConnectionsSearching": "Searching {count} accounts...", 1849 + "@formatConnectionsSearching": { 1850 + "description": "Search progress message for profile connections", 1851 + "placeholders": { 1852 + "count": { 1853 + "type": "int", 1854 + "example": "25" 1855 + } 1856 + } 1857 + }, 1858 + "formatConnectionsSearched": "Searched {count} accounts", 1859 + "@formatConnectionsSearched": { 1860 + "description": "Completed search progress message for profile connections", 1861 + "placeholders": { 1862 + "count": { 1863 + "type": "int", 1864 + "example": "25" 1865 + } 1866 + } 1867 + }, 1868 + "formatConnectionsSearchStopped": "Search stopped after {count} accounts", 1869 + "@formatConnectionsSearchStopped": { 1870 + "description": "Stopped search progress message for profile connections", 1871 + "placeholders": { 1872 + "count": { 1873 + "type": "int", 1874 + "example": "25" 1875 + } 1876 + } 1877 + }, 1878 + "formatClassifyingProgress": "Classifying: {progress}/{total}", 1879 + "@formatClassifyingProgress": { 1880 + "description": "Follow audit classifying progress label", 1881 + "placeholders": { 1882 + "progress": { 1883 + "type": "int", 1884 + "example": "20" 1885 + }, 1886 + "total": { 1887 + "type": "int", 1888 + "example": "120" 1889 + } 1890 + } 1891 + }, 1892 + "formatDidCopied": "DID copied to clipboard", 1893 + "@formatDidCopied": { 1894 + "description": "Snackbar message after copying a DID" 1895 + }, 1896 + "formatFetchingFollowsProgress": "Fetching follows: {progress}/{total}", 1897 + "@formatFetchingFollowsProgress": { 1898 + "description": "Follow audit fetching progress label", 1899 + "placeholders": { 1900 + "progress": { 1901 + "type": "int", 1902 + "example": "20" 1903 + }, 1904 + "total": { 1905 + "type": "int", 1906 + "example": "120" 1907 + } 1908 + } 1909 + }, 1910 + "formatFollowedMemberCount": "Followed {count, plural, =1{1 member} other{{count} members}}", 1911 + "@formatFollowedMemberCount": { 1912 + "description": "Snackbar message after following starter pack members", 1913 + "placeholders": { 1914 + "count": { 1915 + "type": "int", 1916 + "example": "8" 1917 + } 1918 + } 1919 + }, 1920 + "formatFollowsScanned": "{count, plural, =1{1 follow scanned for problematic accounts} other{{count} follows scanned for problematic accounts}}", 1921 + "@formatFollowsScanned": { 1922 + "description": "Follow audit summary after scanning follows", 1923 + "placeholders": { 1924 + "count": { 1925 + "type": "int", 1926 + "example": "245" 1927 + } 1928 + } 1929 + }, 1930 + "formatHideStatus": "Hide {status}", 1931 + "@formatHideStatus": { 1932 + "description": "Tooltip to hide a follow audit status", 1933 + "placeholders": { 1934 + "status": { 1935 + "type": "String", 1936 + "example": "Deleted" 1937 + } 1938 + } 1939 + }, 1940 + "formatJoinedDate": "Joined {date}", 1941 + "@formatJoinedDate": { 1942 + "description": "Profile joined date label", 1943 + "placeholders": { 1944 + "date": { 1945 + "type": "String", 1946 + "example": "March 2026" 1947 + } 1948 + } 1949 + }, 1950 + "formatJoinedRelative": "Joined {relativeTime}", 1951 + "@formatJoinedRelative": { 1952 + "description": "Profile card joined relative time label", 1953 + "placeholders": { 1954 + "relativeTime": { 1955 + "type": "String", 1956 + "example": "2mo ago" 1957 + } 1958 + } 1959 + }, 1960 + "formatListByHandle": "by @{handle}", 1961 + "@formatListByHandle": { 1962 + "description": "List creator attribution label", 1963 + "placeholders": { 1964 + "handle": { 1965 + "type": "String", 1966 + "example": "alice.bsky.social" 1967 + } 1968 + } 1969 + }, 1970 + "formatMemberCount": "{count, plural, =1{1 member} other{{count} members}}", 1971 + "@formatMemberCount": { 1972 + "description": "Count of list or starter pack members", 1973 + "placeholders": { 1974 + "count": { 1975 + "type": "int", 1976 + "example": "12" 1977 + } 1978 + } 1979 + }, 1980 + "formatProfileReportTitle": "{title} by @{handle}", 1981 + "@formatProfileReportTitle": { 1982 + "description": "Report dialog title for a target handle", 1983 + "placeholders": { 1984 + "title": { 1985 + "type": "String", 1986 + "example": "Report Post" 1987 + }, 1988 + "handle": { 1989 + "type": "String", 1990 + "example": "alice.bsky.social" 1991 + } 1992 + } 1993 + }, 1994 + "formatProfileTextLimit": "{label} must be {count} characters or fewer", 1995 + "@formatProfileTextLimit": { 1996 + "description": "Profile edit validation message for a text length limit", 1997 + "placeholders": { 1998 + "label": { 1999 + "type": "String", 2000 + "example": "Display name" 2001 + }, 2002 + "count": { 2003 + "type": "int", 2004 + "example": "64" 2005 + } 2006 + } 2007 + }, 2008 + "formatProfileTextTooLong": "{label} is too long", 2009 + "@formatProfileTextTooLong": { 2010 + "description": "Profile edit validation message for byte length", 2011 + "placeholders": { 2012 + "label": { 2013 + "type": "String", 2014 + "example": "Description" 2015 + } 2016 + } 2017 + }, 2018 + "formatProfilesFailedToLoad": "{count, plural, =1{1 profile could not be loaded.} other{{count} profiles could not be loaded.}}", 2019 + "@formatProfilesFailedToLoad": { 2020 + "description": "Follow audit warning when some profiles failed to load", 2021 + "placeholders": { 2022 + "count": { 2023 + "type": "int", 2024 + "example": "3" 2025 + } 2026 + } 2027 + }, 2028 + "formatReportSubmitted": "Thank you. Your report (ID: {reportId}) has been submitted.", 2029 + "@formatReportSubmitted": { 2030 + "description": "Report submission success dialog body", 2031 + "placeholders": { 2032 + "reportId": { 2033 + "type": "String", 2034 + "example": "3lkgvbn" 2035 + } 2036 + } 2037 + }, 2038 + "formatSelectedCount": "Selected: {selected}/{total}", 2039 + "@formatSelectedCount": { 2040 + "description": "Follow audit selected count footer", 2041 + "placeholders": { 2042 + "selected": { 2043 + "type": "int", 2044 + "example": "2" 2045 + }, 2046 + "total": { 2047 + "type": "int", 2048 + "example": "12" 2049 + } 2050 + } 2051 + }, 2052 + "formatShowStatus": "Show {status}", 2053 + "@formatShowStatus": { 2054 + "description": "Tooltip to show a follow audit status", 2055 + "placeholders": { 2056 + "status": { 2057 + "type": "String", 2058 + "example": "Deleted" 2059 + } 2060 + } 2061 + }, 2062 + "formatUnavailableAccounts": "Unavailable accounts ({count})", 2063 + "@formatUnavailableAccounts": { 2064 + "description": "Title for unavailable accounts card", 2065 + "placeholders": { 2066 + "count": { 2067 + "type": "int", 2068 + "example": "3" 2069 + } 2070 + } 2071 + }, 2072 + "formatUnfollowedAccounts": "Unfollowed {count} account(s)", 2073 + "@formatUnfollowedAccounts": { 2074 + "description": "Follow audit completion message", 2075 + "placeholders": { 2076 + "count": { 2077 + "type": "int", 2078 + "example": "5" 2079 + } 2080 + } 2081 + }, 2082 + "formatValidationRequiredMaxCharacters": "Required, max {count} characters", 2083 + "@formatValidationRequiredMaxCharacters": { 2084 + "description": "Helper text for required character-limited fields", 2085 + "placeholders": { 2086 + "count": { 2087 + "type": "int", 2088 + "example": "50" 2089 + } 2090 + } 2091 + }, 2092 + "labelAddToList": "Add to list", 2093 + "@labelAddToList": { 2094 + "description": "Action label to add an account to a list" 2095 + }, 2096 + "labelAuditFollowers": "Audit Followers", 2097 + "@labelAuditFollowers": { 2098 + "description": "Follow audit screen title" 2099 + }, 2100 + "labelBanner": "Banner", 2101 + "@labelBanner": { 2102 + "description": "Profile banner image button label" 2103 + }, 2104 + "labelBlockViaList": "Block via list", 2105 + "@labelBlockViaList": { 2106 + "description": "List action label to block accounts via a moderation list" 2107 + }, 2108 + "labelBlockedBy": "Blocked By", 2109 + "@labelBlockedBy": { 2110 + "description": "Profile context tab label for accounts that blocked the user" 2111 + }, 2112 + "labelBlocking": "Blocking", 2113 + "@labelBlocking": { 2114 + "description": "Profile context tab label for accounts the user is blocking" 2115 + }, 2116 + "labelConnections": "Connections", 2117 + "@labelConnections": { 2118 + "description": "Profile connections screen title" 2119 + }, 2120 + "labelCopyDid": "Copy DID", 2121 + "@labelCopyDid": { 2122 + "description": "Profile action label to copy DID" 2123 + }, 2124 + "labelCreateList": "Create list", 2125 + "@labelCreateList": { 2126 + "description": "Create list dialog title" 2127 + }, 2128 + "labelCreateStarterPack": "Create starter pack", 2129 + "@labelCreateStarterPack": { 2130 + "description": "Tooltip to create a starter pack" 2131 + }, 2132 + "labelCurateShort": "CURATE", 2133 + "@labelCurateShort": { 2134 + "description": "Short curation list badge label" 2135 + }, 2136 + "labelCurrentMembers": "Current Members", 2137 + "@labelCurrentMembers": { 2138 + "description": "List members section heading" 2139 + }, 2140 + "labelCurationLists": "Curation Lists", 2141 + "@labelCurationLists": { 2142 + "description": "Profile context list section heading" 2143 + }, 2144 + "labelDescription": "Description", 2145 + "@labelDescription": { 2146 + "description": "Description field label" 2147 + }, 2148 + "labelDescriptionOptional": "Description (optional)", 2149 + "@labelDescriptionOptional": { 2150 + "description": "Optional description field label" 2151 + }, 2152 + "labelDisplayName": "Display name", 2153 + "@labelDisplayName": { 2154 + "description": "Profile display name field label" 2155 + }, 2156 + "labelEditList": "Edit list", 2157 + "@labelEditList": { 2158 + "description": "Edit list dialog or action title" 2159 + }, 2160 + "labelEditProfile": "Edit profile", 2161 + "@labelEditProfile": { 2162 + "description": "Edit profile screen title or action label" 2163 + }, 2164 + "labelEditStarterPack": "Edit starter pack", 2165 + "@labelEditStarterPack": { 2166 + "description": "Edit starter pack dialog title" 2167 + }, 2168 + "labelFeed": "Feed", 2169 + "@labelFeed": { 2170 + "description": "Feed label" 2171 + }, 2172 + "labelFollowers": "Followers", 2173 + "@labelFollowers": { 2174 + "description": "Followers label" 2175 + }, 2176 + "labelJoinedThisWeek": "joined this week", 2177 + "@labelJoinedThisWeek": { 2178 + "description": "Starter pack statistic label for joins this week" 2179 + }, 2180 + "labelJoinedTotal": "joined total", 2181 + "@labelJoinedTotal": { 2182 + "description": "Starter pack card statistic label for total joins" 2183 + }, 2184 + "labelFollowing": "Following", 2185 + "@labelFollowing": { 2186 + "description": "Following label" 2187 + }, 2188 + "labelList": "List", 2189 + "@labelList": { 2190 + "description": "Generic list title" 2191 + }, 2192 + "labelLists": "Lists", 2193 + "@labelLists": { 2194 + "description": "Lists label" 2195 + }, 2196 + "labelMedia": "Media", 2197 + "@labelMedia": { 2198 + "description": "Profile media tab label" 2199 + }, 2200 + "labelMembers": "Members", 2201 + "@labelMembers": { 2202 + "description": "Members section label" 2203 + }, 2204 + "labelModerationLists": "Moderation Lists", 2205 + "@labelModerationLists": { 2206 + "description": "Profile context moderation list section heading" 2207 + }, 2208 + "labelModerationShort": "MOD", 2209 + "@labelModerationShort": { 2210 + "description": "Short moderation list badge label" 2211 + }, 2212 + "labelMuteList": "Mute list", 2213 + "@labelMuteList": { 2214 + "description": "List action label to mute a list" 2215 + }, 2216 + "labelMutuals": "Mutuals", 2217 + "@labelMutuals": { 2218 + "description": "Mutual follows label" 2219 + }, 2220 + "labelMyLists": "My Lists", 2221 + "@labelMyLists": { 2222 + "description": "My lists screen title" 2223 + }, 2224 + "labelName": "Name", 2225 + "@labelName": { 2226 + "description": "Name field label" 2227 + }, 2228 + "labelNewStarterPack": "New Starter Pack", 2229 + "@labelNewStarterPack": { 2230 + "description": "New starter pack screen title" 2231 + }, 2232 + "labelOtherLists": "Other Lists", 2233 + "@labelOtherLists": { 2234 + "description": "Profile context other list section heading" 2235 + }, 2236 + "labelPronouns": "Pronouns", 2237 + "@labelPronouns": { 2238 + "description": "Pronouns field label" 2239 + }, 2240 + "labelProfileContext": "Profile Context", 2241 + "@labelProfileContext": { 2242 + "description": "Profile context screen title and action label" 2243 + }, 2244 + "labelProfileTitle": "Profile", 2245 + "@labelProfileTitle": { 2246 + "description": "Generic profile screen title" 2247 + }, 2248 + "labelRecommendedFeeds": "Recommended Feeds", 2249 + "@labelRecommendedFeeds": { 2250 + "description": "Starter pack feeds section title" 2251 + }, 2252 + "labelReferenceLists": "Reference Lists", 2253 + "@labelReferenceLists": { 2254 + "description": "Profile context reference list section heading" 2255 + }, 2256 + "labelReferenceShort": "REFERENCE", 2257 + "@labelReferenceShort": { 2258 + "description": "Short reference list badge label" 2259 + }, 2260 + "labelReplies": "Replies", 2261 + "@labelReplies": { 2262 + "description": "Profile replies tab label" 2263 + }, 2264 + "labelReport": "Report", 2265 + "@labelReport": { 2266 + "description": "Report action label" 2267 + }, 2268 + "labelReportAccount": "Report Account", 2269 + "@labelReportAccount": { 2270 + "description": "Report account dialog title" 2271 + }, 2272 + "labelReportReason": "Reason", 2273 + "@labelReportReason": { 2274 + "description": "Report reason section label" 2275 + }, 2276 + "labelReportReasonExplanationRequired": "Explanation (required)", 2277 + "@labelReportReasonExplanationRequired": { 2278 + "description": "Report explanation field label" 2279 + }, 2280 + "labelReportReasonHarassment": "Harassment", 2281 + "@labelReportReasonHarassment": { 2282 + "description": "Report reason label for harassment" 2283 + }, 2284 + "labelReportReasonMisleading": "Misleading", 2285 + "@labelReportReasonMisleading": { 2286 + "description": "Report reason label for misleading content" 2287 + }, 2288 + "labelReportReasonOther": "Other", 2289 + "@labelReportReasonOther": { 2290 + "description": "Report reason label for other" 2291 + }, 2292 + "labelReportReasonSexualContent": "Sexual Content", 2293 + "@labelReportReasonSexualContent": { 2294 + "description": "Report reason label for sexual content" 2295 + }, 2296 + "labelReportReasonSpam": "Spam", 2297 + "@labelReportReasonSpam": { 2298 + "description": "Report reason label for spam" 2299 + }, 2300 + "labelReportReasonViolation": "Violation", 2301 + "@labelReportReasonViolation": { 2302 + "description": "Report reason label for violations" 2303 + }, 2304 + "labelReportSubmitted": "Report Submitted", 2305 + "@labelReportSubmitted": { 2306 + "description": "Report success dialog title" 2307 + }, 2308 + "labelSelectAll": "Select All", 2309 + "@labelSelectAll": { 2310 + "description": "Select all checkbox label" 2311 + }, 2312 + "labelSelectFeed": "Select a feed", 2313 + "@labelSelectFeed": { 2314 + "description": "Feed picker sheet title" 2315 + }, 2316 + "labelShareProfile": "Share Profile", 2317 + "@labelShareProfile": { 2318 + "description": "Profile action label to share a profile" 2319 + }, 2320 + "labelStarterPack": "Starter Pack", 2321 + "@labelStarterPack": { 2322 + "description": "Fallback starter pack title" 2323 + }, 2324 + "labelType": "Type", 2325 + "@labelType": { 2326 + "description": "Type field label" 2327 + }, 2328 + "labelTotalJoined": "total joined", 2329 + "@labelTotalJoined": { 2330 + "description": "Starter pack detail statistic label for total joins" 2331 + }, 2332 + "labelUnavailableLikedPost": "Unavailable liked post", 2333 + "@labelUnavailableLikedPost": { 2334 + "description": "Profile liked posts unavailable entry title" 2335 + }, 2336 + "labelUnblockViaList": "Unblock via list", 2337 + "@labelUnblockViaList": { 2338 + "description": "List action label to unblock accounts via a moderation list" 2339 + }, 2340 + "labelUnmuteList": "Unmute list", 2341 + "@labelUnmuteList": { 2342 + "description": "List action label to unmute a list" 2343 + }, 2344 + "labelUpToThree": "(up to 3)", 2345 + "@labelUpToThree": { 2346 + "description": "Helper label for selecting up to three starter pack feeds" 2347 + }, 2348 + "labelWebsite": "Website", 2349 + "@labelWebsite": { 2350 + "description": "Website field label" 2351 + }, 2352 + "labelYou": "You", 2353 + "@labelYou": { 2354 + "description": "Profile connection chip for the current user" 2355 + }, 2356 + "messageBlockedByContextNotice": "Blocks are a normal part of social media. This data is public on the AT Protocol.", 2357 + "@messageBlockedByContextNotice": { 2358 + "description": "Profile context blocked-by explanatory copy" 2359 + }, 2360 + "messageBlockingOnlyOwnProfile": "Blocking information is only available when viewing your own profile.", 2361 + "@messageBlockingOnlyOwnProfile": { 2362 + "description": "Profile context message when blocking list is not available" 2363 + }, 2364 + "messageChangeAvatarImage": "Change avatar image", 2365 + "@messageChangeAvatarImage": { 2366 + "description": "Tooltip and semantics label for changing profile avatar" 2367 + }, 2368 + "messageChangeBannerImage": "Change banner image", 2369 + "@messageChangeBannerImage": { 2370 + "description": "Tooltip and semantics label for changing profile banner" 2371 + }, 2372 + "messageEnterValidWebsite": "Enter a valid website", 2373 + "@messageEnterValidWebsite": { 2374 + "description": "Profile edit validation error for invalid website" 2375 + }, 2376 + "messageFeedUnavailableForModerationLists": "Feed not available for moderation lists", 2377 + "@messageFeedUnavailableForModerationLists": { 2378 + "description": "List detail message when moderation lists do not have feeds" 2379 + }, 2380 + "messageFollowAuditIntro": "Scan your follows for deleted, suspended, blocked, and hidden accounts.", 2381 + "@messageFollowAuditIntro": { 2382 + "description": "Follow audit intro message before scanning" 2383 + }, 2384 + "messageFollowAuditStartPrompt": "Tap Scan to audit your follow list.", 2385 + "@messageFollowAuditStartPrompt": { 2386 + "description": "Follow audit empty prompt before scanning" 2387 + }, 2388 + "messageNoAccountsBlockedThisUser": "No accounts have blocked this user", 2389 + "@messageNoAccountsBlockedThisUser": { 2390 + "description": "Profile context empty blocked-by message" 2391 + }, 2392 + "messageNoListsYet": "No lists yet", 2393 + "@messageNoListsYet": { 2394 + "description": "Empty state when no lists exist" 2395 + }, 2396 + "messageNoMembers": "No members", 2397 + "@messageNoMembers": { 2398 + "description": "Empty starter pack members message" 2399 + }, 2400 + "messageNoMembersYet": "No members yet", 2401 + "@messageNoMembersYet": { 2402 + "description": "Empty list members message" 2403 + }, 2404 + "messageNoMembersYetSearch": "No members yet. Search above to add people.", 2405 + "@messageNoMembersYetSearch": { 2406 + "description": "Empty list members message with search instruction" 2407 + }, 2408 + "messageNoMediaPostsYet": "No media posts yet", 2409 + "@messageNoMediaPostsYet": { 2410 + "description": "Empty profile media tab message" 2411 + }, 2412 + "messageNoPostsYet": "No posts yet", 2413 + "@messageNoPostsYet": { 2414 + "description": "Empty profile/list feed message" 2415 + }, 2416 + "messageNoProblematicFollows": "No problematic follows found", 2417 + "@messageNoProblematicFollows": { 2418 + "description": "Follow audit empty results message" 2419 + }, 2420 + "messageNoRepliesYet": "No replies yet", 2421 + "@messageNoRepliesYet": { 2422 + "description": "Empty profile replies message" 2423 + }, 2424 + "messageNoResultsForFilters": "No results visible for the current filters.", 2425 + "@messageNoResultsForFilters": { 2426 + "description": "Follow audit empty filtered results message" 2427 + }, 2428 + "messageNoStarterPacksYet": "No starter packs yet", 2429 + "@messageNoStarterPacksYet": { 2430 + "description": "Empty starter packs message" 2431 + }, 2432 + "messageNoSuggestionsFound": "No suggestions found", 2433 + "@messageNoSuggestionsFound": { 2434 + "description": "Empty suggested follows message" 2435 + }, 2436 + "messageNotBlockingAnyone": "Not blocking anyone", 2437 + "@messageNotBlockingAnyone": { 2438 + "description": "Profile context empty blocking message" 2439 + }, 2440 + "messageNotOnAnyLists": "Not on any lists", 2441 + "@messageNotOnAnyLists": { 2442 + "description": "Profile context empty lists-on message" 2443 + }, 2444 + "messageProfileUnavailable": "Profile unavailable", 2445 + "@messageProfileUnavailable": { 2446 + "description": "Fallback unavailable profile reason" 2447 + }, 2448 + "messageProfileUpdated": "Profile updated", 2449 + "@messageProfileUpdated": { 2450 + "description": "Snackbar after profile update succeeds" 2451 + }, 2452 + "messageReportExplanationHint": "Please explain why you are reporting this...", 2453 + "@messageReportExplanationHint": { 2454 + "description": "Report explanation text field hint" 2455 + }, 2456 + "messageReportReasonHarassmentDescription": "Harassment or rude behaviour", 2457 + "@messageReportReasonHarassmentDescription": { 2458 + "description": "Report reason description for harassment" 2459 + }, 2460 + "messageReportReasonMisleadingDescription": "Misleading or deceptive content", 2461 + "@messageReportReasonMisleadingDescription": { 2462 + "description": "Report reason description for misleading content" 2463 + }, 2464 + "messageReportReasonOtherDescription": "Other reason (requires explanation)", 2465 + "@messageReportReasonOtherDescription": { 2466 + "description": "Report reason description for other" 2467 + }, 2468 + "messageReportReasonSexualContentDescription": "Unwanted sexual content", 2469 + "@messageReportReasonSexualContentDescription": { 2470 + "description": "Report reason description for sexual content" 2471 + }, 2472 + "messageReportReasonSpamDescription": "Spam or unsolicited content", 2473 + "@messageReportReasonSpamDescription": { 2474 + "description": "Report reason description for spam" 2475 + }, 2476 + "messageReportReasonViolationDescription": "Violates community guidelines", 2477 + "@messageReportReasonViolationDescription": { 2478 + "description": "Report reason description for violations" 2479 + }, 2480 + "messageSearchConnectionsPlaceholder": "Search handle, name, or description", 2481 + "@messageSearchConnectionsPlaceholder": { 2482 + "description": "Profile connections search field placeholder" 2483 + }, 2484 + "messageSearchPeopleToAddPlaceholder": "Search for people to add", 2485 + "@messageSearchPeopleToAddPlaceholder": { 2486 + "description": "Search field placeholder when adding people" 2487 + }, 2488 + "messageSomeBlockedAccountsUnavailable": "Some blocked accounts are suspended or unavailable.", 2489 + "@messageSomeBlockedAccountsUnavailable": { 2490 + "description": "Profile context message when some blocked accounts are unavailable" 2491 + }, 2492 + "messageSuggestedFollowsUnavailable": "Suggested follows are unavailable right now.", 2493 + "@messageSuggestedFollowsUnavailable": { 2494 + "description": "Suggested follows unavailable message" 2495 + }, 2496 + "messageUnavailableAccountsDescription": "These accounts are suspended or their public profile could not be fetched.", 2497 + "@messageUnavailableAccountsDescription": { 2498 + "description": "Unavailable accounts card subtitle" 2499 + }, 2500 + "statusBlockedBy": "Blocked by", 2501 + "@statusBlockedBy": { 2502 + "description": "Follow audit status label" 2503 + }, 2504 + "statusBlocking": "Blocking", 2505 + "@statusBlocking": { 2506 + "description": "Follow audit status label" 2507 + }, 2508 + "statusDeactivated": "Deactivated", 2509 + "@statusDeactivated": { 2510 + "description": "Follow audit status label" 2511 + }, 2512 + "statusDeleted": "Deleted", 2513 + "@statusDeleted": { 2514 + "description": "Follow audit status label" 2515 + }, 2516 + "statusHidden": "Hidden", 2517 + "@statusHidden": { 2518 + "description": "Follow audit status label" 2519 + }, 2520 + "statusMutualBlock": "Mutual block", 2521 + "@statusMutualBlock": { 2522 + "description": "Follow audit status label" 2523 + }, 2524 + "statusSelfFollow": "Self-follow", 2525 + "@statusSelfFollow": { 2526 + "description": "Follow audit status label" 2527 + }, 2528 + "statusSuspended": "Suspended", 2529 + "@statusSuspended": { 2530 + "description": "Follow audit status label" 2531 + }, 2532 + "tooltipClearSearch": "Clear search", 2533 + "@tooltipClearSearch": { 2534 + "description": "Tooltip for clearing a search field" 2535 + }, 2536 + "tooltipJumpToTop": "Jump to top", 2537 + "@tooltipJumpToTop": { 2538 + "description": "Tooltip for jump to top button" 2539 + }, 2540 + "validationEnterValidWebsite": "Enter a valid website", 2541 + "@validationEnterValidWebsite": { 2542 + "description": "Profile edit validation error for invalid website" 2543 + }, 2544 + "accountSwitcherNoOtherAccounts": "No other signed-in accounts yet. Add an account to switch between profiles.", 2545 + "@accountSwitcherNoOtherAccounts": { 2546 + "description": "Empty state message in the account switcher sheet" 2547 + }, 2548 + "buttonAddAccount": "Add Account", 2549 + "@buttonAddAccount": { 2550 + "description": "Button and dialog title to add another account" 2551 + }, 2552 + "buttonCopyAll": "Copy All", 2553 + "@buttonCopyAll": { 2554 + "description": "Message thread menu item to copy all messages" 2555 + }, 2556 + "buttonMarkAllRead": "Mark All Read", 2557 + "@buttonMarkAllRead": { 2558 + "description": "Button label to mark notifications read" 2559 + }, 2560 + "errorFailedToAddAccount": "Failed to add account", 2561 + "@errorFailedToAddAccount": { 2562 + "description": "Snackbar message when adding an account fails" 2563 + }, 2564 + "errorFailedToLoadMessages": "Failed to load messages", 2565 + "@errorFailedToLoadMessages": { 2566 + "description": "Error title when messages cannot load" 2567 + }, 2568 + "errorFailedToLoadNotifications": "Failed to load notifications", 2569 + "@errorFailedToLoadNotifications": { 2570 + "description": "Error title when notifications cannot load" 2571 + }, 2572 + "errorUnableToRemoveAccountNow": "Unable to remove account right now.", 2573 + "@errorUnableToRemoveAccountNow": { 2574 + "description": "Snackbar message when removing an account from account switcher fails" 2575 + }, 2576 + "formatActorListTwo": "{first} and {second}", 2577 + "@formatActorListTwo": { 2578 + "description": "Two-person actor summary in grouped notifications", 2579 + "placeholders": { 2580 + "first": { 2581 + "type": "String", 2582 + "example": "Alice" 2583 + }, 2584 + "second": { 2585 + "type": "String", 2586 + "example": "Bob" 2587 + } 2588 + } 2589 + }, 2590 + "formatActorListWithOthers": "{first}, {second}, and {count} others", 2591 + "@formatActorListWithOthers": { 2592 + "description": "Multi-person actor summary in grouped notifications", 2593 + "placeholders": { 2594 + "first": { 2595 + "type": "String", 2596 + "example": "Alice" 2597 + }, 2598 + "second": { 2599 + "type": "String", 2600 + "example": "Bob" 2601 + }, 2602 + "count": { 2603 + "type": "int", 2604 + "example": "3" 2605 + } 2606 + } 2607 + }, 2608 + "formatMonthDay": "{month} {day}", 2609 + "@formatMonthDay": { 2610 + "description": "Month and day label for notification sections", 2611 + "placeholders": { 2612 + "month": { 2613 + "type": "String", 2614 + "example": "January" 2615 + }, 2616 + "day": { 2617 + "type": "int", 2618 + "example": "8" 2619 + } 2620 + } 2621 + }, 2622 + "formatRemoveAccountContent": "Remove @{handle} from this device?", 2623 + "@formatRemoveAccountContent": { 2624 + "description": "Confirmation dialog body when removing an account from the account switcher", 2625 + "placeholders": { 2626 + "handle": { 2627 + "type": "String", 2628 + "example": "alice.bsky.social" 2629 + } 2630 + } 2631 + }, 2632 + "labelAccounts": "Accounts", 2633 + "@labelAccounts": { 2634 + "description": "Account switcher sheet title" 2635 + }, 2636 + "labelAlertsTitle": "Alerts", 2637 + "@labelAlertsTitle": { 2638 + "description": "Alerts screen title" 2639 + }, 2640 + "labelConversation": "Conversation", 2641 + "@labelConversation": { 2642 + "description": "Fallback conversation title" 2643 + }, 2644 + "labelFollows": "Follows", 2645 + "@labelFollows": { 2646 + "description": "Notification channel name for follows" 2647 + }, 2648 + "labelLikes": "Likes", 2649 + "@labelLikes": { 2650 + "description": "Notification channel name for likes" 2651 + }, 2652 + "labelMessageRequests": "Requests", 2653 + "@labelMessageRequests": { 2654 + "description": "Message requests tab label" 2655 + }, 2656 + "labelOther": "Other", 2657 + "@labelOther": { 2658 + "description": "Other notification channel name" 2659 + }, 2660 + "labelPrimary": "Primary", 2661 + "@labelPrimary": { 2662 + "description": "Primary messages tab label" 2663 + }, 2664 + "labelSomeone": "Someone", 2665 + "@labelSomeone": { 2666 + "description": "Fallback actor summary for grouped notifications" 2667 + }, 2668 + "messageCopied": "Message copied", 2669 + "@messageCopied": { 2670 + "description": "Snackbar message after copying a message" 2671 + }, 2672 + "messageDeleted": "Message deleted", 2673 + "@messageDeleted": { 2674 + "description": "Deleted message placeholder" 2675 + }, 2676 + "messageLocalNotificationFallbackBody": "sent a notification", 2677 + "@messageLocalNotificationFallbackBody": { 2678 + "description": "Local notification body fallback for unknown notification reasons" 2679 + }, 2680 + "messageNewNotification": "New notification", 2681 + "@messageNewNotification": { 2682 + "description": "Local notification title fallback" 2683 + }, 2684 + "messageNoConnection": "No connection", 2685 + "@messageNoConnection": { 2686 + "description": "Offline state title" 2687 + }, 2688 + "messageNoConversationsYet": "No conversations yet", 2689 + "@messageNoConversationsYet": { 2690 + "description": "Empty primary conversations state" 2691 + }, 2692 + "messageNoMessageRequests": "No message requests", 2693 + "@messageNoMessageRequests": { 2694 + "description": "Empty message requests state" 2695 + }, 2696 + "messageNoMessagesYet": "No messages yet", 2697 + "@messageNoMessagesYet": { 2698 + "description": "Empty message thread state" 2699 + }, 2700 + "messageNoNotificationsYet": "No notifications yet", 2701 + "@messageNoNotificationsYet": { 2702 + "description": "Empty notifications state" 2703 + }, 2704 + "messageNotificationContactMatch": "joined from your contacts", 2705 + "@messageNotificationContactMatch": { 2706 + "description": "Notification summary for contact match" 2707 + }, 2708 + "messageNotificationFollow": "followed you", 2709 + "@messageNotificationFollow": { 2710 + "description": "Notification summary for follows" 2711 + }, 2712 + "messageNotificationInteracted": "interacted with you", 2713 + "@messageNotificationInteracted": { 2714 + "description": "Notification summary fallback" 2715 + }, 2716 + "messageNotificationLike": "liked your post", 2717 + "@messageNotificationLike": { 2718 + "description": "Notification summary for likes" 2719 + }, 2720 + "messageNotificationLikeViaRepost": "liked your repost", 2721 + "@messageNotificationLikeViaRepost": { 2722 + "description": "Notification summary for likes via repost" 2723 + }, 2724 + "messageNotificationMention": "mentioned you", 2725 + "@messageNotificationMention": { 2726 + "description": "Notification summary for mentions" 2727 + }, 2728 + "messageNotificationQuote": "quoted your post", 2729 + "@messageNotificationQuote": { 2730 + "description": "Notification summary for quotes" 2731 + }, 2732 + "messageNotificationReply": "replied to your post", 2733 + "@messageNotificationReply": { 2734 + "description": "Notification summary for replies" 2735 + }, 2736 + "messageNotificationRepost": "reposted your post", 2737 + "@messageNotificationRepost": { 2738 + "description": "Notification summary for reposts" 2739 + }, 2740 + "messageNotificationRepostViaRepost": "reposted your repost", 2741 + "@messageNotificationRepostViaRepost": { 2742 + "description": "Notification summary for reposts via repost" 2743 + }, 2744 + "messageNotificationStarterPackJoined": "joined via your starter pack", 2745 + "@messageNotificationStarterPackJoined": { 2746 + "description": "Notification summary for starter pack joins" 2747 + }, 2748 + "messageNotificationSubscribedPost": "posted a new update", 2749 + "@messageNotificationSubscribedPost": { 2750 + "description": "Notification summary for subscribed posts" 2751 + }, 2752 + "messageNotificationUnverified": "removed your verification", 2753 + "@messageNotificationUnverified": { 2754 + "description": "Notification summary for unverified account" 2755 + }, 2756 + "messageNotificationVerified": "verified your account", 2757 + "@messageNotificationVerified": { 2758 + "description": "Notification summary for verified account" 2759 + }, 2760 + "messagePlaceholder": "Message…", 2761 + "@messagePlaceholder": { 2762 + "description": "Message composer placeholder" 2763 + }, 2764 + "messagePleaseSignInAgainForAccount": "Please sign in again for that account.", 2765 + "@messagePleaseSignInAgainForAccount": { 2766 + "description": "Snackbar when account switch requires reauthentication" 2767 + }, 2768 + "messageReconnectToLoadMessages": "Reconnect to load messages.", 2769 + "@messageReconnectToLoadMessages": { 2770 + "description": "Offline message list explanation" 2771 + }, 2772 + "messageReconnectToLoadNotifications": "Reconnect to load notifications.", 2773 + "@messageReconnectToLoadNotifications": { 2774 + "description": "Offline notifications explanation" 2775 + }, 2776 + "messageThreadCopied": "Thread copied", 2777 + "@messageThreadCopied": { 2778 + "description": "Snackbar after copying a message thread" 2779 + }, 2780 + "messageToday": "Today", 2781 + "@messageToday": { 2782 + "description": "Today date section label" 2783 + }, 2784 + "messageYesterday": "Yesterday", 2785 + "@messageYesterday": { 2786 + "description": "Yesterday date section label" 2787 + }, 2788 + "placeholderUsernameBskySocial": "username.bsky.social", 2789 + "@placeholderUsernameBskySocial": { 2790 + "description": "Handle placeholder in account switcher" 2791 + }, 2792 + "validationEnterBlueskyHandleOrDid": "Enter a Bluesky handle or DID", 2793 + "@validationEnterBlueskyHandleOrDid": { 2794 + "description": "Account switcher validation error for empty handle or DID" 2795 + }, 2796 + "validationEnterCompleteDid": "Enter a complete DID like did:plc:... or did:web:...", 2797 + "@validationEnterCompleteDid": { 2798 + "description": "Account switcher validation error for incomplete DID" 2799 + }, 2800 + "validationEnterFullHandle": "Enter a full handle like username.bsky.social", 2801 + "@validationEnterFullHandle": { 2802 + "description": "Account switcher validation error for invalid handle" 2803 + }, 2804 + "validationUseSupportedDid": "Use a did:plc:... or did:web:... identifier", 2805 + "@validationUseSupportedDid": { 2806 + "description": "Account switcher validation error for unsupported DID method" 2807 + }, 772 2808 "validationEnterAppPassword": "Enter your app password", 773 2809 "@validationEnterAppPassword": { 774 2810 "description": "Validation error for missing app password" 2811 + }, 2812 + "buttonAdd": "Add", 2813 + "@buttonAdd": { 2814 + "description": "Generic add button label" 2815 + }, 2816 + "buttonAdding": "Adding...", 2817 + "@buttonAdding": { 2818 + "description": "Button label while an add action is in progress" 2819 + }, 2820 + "buttonInspiredByPdsLs": "Inspired by pds.ls", 2821 + "@buttonInspiredByPdsLs": { 2822 + "description": "Devtools button label linking to pds.ls" 2823 + }, 2824 + "buttonCopyJson": "Copy JSON", 2825 + "@buttonCopyJson": { 2826 + "description": "Button label to copy record JSON" 2827 + }, 2828 + "buttonResolve": "Resolve", 2829 + "@buttonResolve": { 2830 + "description": "Button label to resolve a handle, DID, or AT URI" 2831 + }, 2832 + "buttonUnsubscribe": "Unsubscribe", 2833 + "@buttonUnsubscribe": { 2834 + "description": "Button label to unsubscribe from a labeler" 2835 + }, 2836 + "dialogAddLabelerTitle": "Add labeler", 2837 + "@dialogAddLabelerTitle": { 2838 + "description": "Dialog title for adding a moderation labeler" 2839 + }, 2840 + "dialogClearAllLogsContent": "This will permanently delete all log files. This action cannot be undone.", 2841 + "@dialogClearAllLogsContent": { 2842 + "description": "Confirmation dialog body before clearing all log files" 2843 + }, 2844 + "dialogClearAllLogsTitle": "Clear all logs?", 2845 + "@dialogClearAllLogsTitle": { 2846 + "description": "Confirmation dialog title before clearing all log files" 2847 + }, 2848 + "errorFailedToLoadLogs": "Failed to load logs", 2849 + "@errorFailedToLoadLogs": { 2850 + "description": "Error title when logs cannot load" 2851 + }, 2852 + "errorFailedToLoadModerationSettings": "Failed to load moderation settings", 2853 + "@errorFailedToLoadModerationSettings": { 2854 + "description": "Error title when moderation settings cannot load" 2855 + }, 2856 + "errorFailedToUnsubscribeLabeler": "Failed to unsubscribe: {error}", 2857 + "@errorFailedToUnsubscribeLabeler": { 2858 + "description": "Snackbar message when unsubscribing from a labeler fails", 2859 + "placeholders": { 2860 + "error": { 2861 + "type": "Object", 2862 + "example": "network unavailable" 2863 + } 2864 + } 2865 + }, 2866 + "errorFailedToUpdateAdultContent": "Failed to update adult content: {error}", 2867 + "@errorFailedToUpdateAdultContent": { 2868 + "description": "Snackbar message when adult content preference update fails", 2869 + "placeholders": { 2870 + "error": { 2871 + "type": "Object", 2872 + "example": "network unavailable" 2873 + } 2874 + } 2875 + }, 2876 + "errorFailedToUpdateLabelPreference": "Failed to update preference: {error}", 2877 + "@errorFailedToUpdateLabelPreference": { 2878 + "description": "Snackbar message when a label preference update fails", 2879 + "placeholders": { 2880 + "error": { 2881 + "type": "Object", 2882 + "example": "network unavailable" 2883 + } 2884 + } 2885 + }, 2886 + "errorFailedToUpdateLabelerSubscription": "Failed to update subscription: {error}", 2887 + "@errorFailedToUpdateLabelerSubscription": { 2888 + "description": "Snackbar message when a labeler subscription update fails", 2889 + "placeholders": { 2890 + "error": { 2891 + "type": "Object", 2892 + "example": "network unavailable" 2893 + } 2894 + } 2895 + }, 2896 + "errorLabelerDidRequired": "Enter a labeler DID.", 2897 + "@errorLabelerDidRequired": { 2898 + "description": "Validation error when adding a labeler without a DID" 2899 + }, 2900 + "errorLabelerNotFound": "Labeler not found.", 2901 + "@errorLabelerNotFound": { 2902 + "description": "Error thrown when a labeler DID cannot be loaded" 2903 + }, 2904 + "errorNoLabelerFoundForDid": "No labeler found for that DID.", 2905 + "@errorNoLabelerFoundForDid": { 2906 + "description": "Validation error when no labeler exists for the entered DID" 2907 + }, 2908 + "errorUnableToLoadLabeler": "Unable to load labeler", 2909 + "@errorUnableToLoadLabeler": { 2910 + "description": "Error title when a labeler detail screen cannot load" 2911 + }, 2912 + "formatAddLabelerLimit": "Add ({current}/{max})", 2913 + "@formatAddLabelerLimit": { 2914 + "description": "Add labeler button showing current and maximum custom labelers", 2915 + "placeholders": { 2916 + "current": { 2917 + "type": "int", 2918 + "example": "1" 2919 + }, 2920 + "max": { 2921 + "type": "int", 2922 + "example": "20" 2923 + } 2924 + } 2925 + }, 2926 + "formatCollectionsCount": "{count, plural, =1{1 collection} other{{count} collections}}", 2927 + "@formatCollectionsCount": { 2928 + "description": "Devtools repository collection count", 2929 + "placeholders": { 2930 + "count": { 2931 + "type": "int", 2932 + "example": "2" 2933 + } 2934 + } 2935 + }, 2936 + "formatCid": "CID: {cid}", 2937 + "@formatCid": { 2938 + "description": "Devtools record CID label", 2939 + "placeholders": { 2940 + "cid": { 2941 + "type": "String", 2942 + "example": "bafyreicid" 2943 + } 2944 + } 2945 + }, 2946 + "formatCustomLabelCount": "{count, plural, =1{1 custom label} other{{count} custom labels}}", 2947 + "@formatCustomLabelCount": { 2948 + "description": "Moderation labeler detail chip showing custom label count", 2949 + "placeholders": { 2950 + "count": { 2951 + "type": "int", 2952 + "example": "3" 2953 + } 2954 + } 2955 + }, 2956 + "formatDefinitionCount": "{count, plural, =1{1 definition} other{{count} definitions}}", 2957 + "@formatDefinitionCount": { 2958 + "description": "Moderation labeler card chip showing custom definition count", 2959 + "placeholders": { 2960 + "count": { 2961 + "type": "int", 2962 + "example": "2" 2963 + } 2964 + } 2965 + }, 2966 + "formatLoadedRecordsCount": "{count} loaded", 2967 + "@formatLoadedRecordsCount": { 2968 + "description": "Devtools record list count while total is unknown", 2969 + "placeholders": { 2970 + "count": { 2971 + "type": "int", 2972 + "example": "20" 2973 + } 2974 + } 2975 + }, 2976 + "formatLoadedRecordsOfTotal": "{loaded} of {total}", 2977 + "@formatLoadedRecordsOfTotal": { 2978 + "description": "Devtools record list count showing loaded and total records", 2979 + "placeholders": { 2980 + "loaded": { 2981 + "type": "int", 2982 + "example": "20" 2983 + }, 2984 + "total": { 2985 + "type": "int", 2986 + "example": "100" 2987 + } 2988 + } 2989 + }, 2990 + "formatModerationSourceLabel": "{source} label", 2991 + "@formatModerationSourceLabel": { 2992 + "description": "Tooltip description for a moderation label source", 2993 + "placeholders": { 2994 + "source": { 2995 + "type": "String", 2996 + "example": "Bluesky" 2997 + } 2998 + } 2999 + }, 3000 + "formatPolicyBlur": "Blur {value}", 3001 + "@formatPolicyBlur": { 3002 + "description": "Moderation policy chip showing the blur behavior", 3003 + "placeholders": { 3004 + "value": { 3005 + "type": "String", 3006 + "example": "content" 3007 + } 3008 + } 3009 + }, 3010 + "formatPolicyDefault": "Default {value}", 3011 + "@formatPolicyDefault": { 3012 + "description": "Moderation policy chip showing default preference", 3013 + "placeholders": { 3014 + "value": { 3015 + "type": "String", 3016 + "example": "warn" 3017 + } 3018 + } 3019 + }, 3020 + "formatPolicyId": "ID {identifier}", 3021 + "@formatPolicyId": { 3022 + "description": "Moderation policy chip showing a label identifier", 3023 + "placeholders": { 3024 + "identifier": { 3025 + "type": "String", 3026 + "example": "graphic-media" 3027 + } 3028 + } 3029 + }, 3030 + "formatPolicySeverity": "Severity {value}", 3031 + "@formatPolicySeverity": { 3032 + "description": "Moderation policy chip showing severity", 3033 + "placeholders": { 3034 + "value": { 3035 + "type": "String", 3036 + "example": "alert" 3037 + } 3038 + } 3039 + }, 3040 + "formatPublishedValueCount": "{count, plural, =1{1 published value} other{{count} published values}}", 3041 + "@formatPublishedValueCount": { 3042 + "description": "Moderation labeler card chip showing published value count", 3043 + "placeholders": { 3044 + "count": { 3045 + "type": "int", 3046 + "example": "4" 3047 + } 3048 + } 3049 + }, 3050 + "formatRecordsCount": "{count, plural, =1{1 record} other{{count} records}}", 3051 + "@formatRecordsCount": { 3052 + "description": "Devtools repository record count", 3053 + "placeholders": { 3054 + "count": { 3055 + "type": "int", 3056 + "example": "5" 3057 + } 3058 + } 3059 + }, 3060 + "formatSubscribedToLabeler": "Subscribed to {name}", 3061 + "@formatSubscribedToLabeler": { 3062 + "description": "Snackbar after subscribing to a labeler", 3063 + "placeholders": { 3064 + "name": { 3065 + "type": "String", 3066 + "example": "Bluesky Safety" 3067 + } 3068 + } 3069 + }, 3070 + "labelAdultContentSetting": "Adult content", 3071 + "@labelAdultContentSetting": { 3072 + "description": "Moderation settings adult content switch label" 3073 + }, 3074 + "labelAdultOnlyShort": "18+", 3075 + "@labelAdultOnlyShort": { 3076 + "description": "Short chip label for adult-only moderation labels" 3077 + }, 3078 + "labelAlwaysOn": "Always on", 3079 + "@labelAlwaysOn": { 3080 + "description": "Status label for always-on moderation labeler" 3081 + }, 3082 + "labelAutoScroll": "Auto-scroll", 3083 + "@labelAutoScroll": { 3084 + "description": "Log viewer auto-scroll control label" 3085 + }, 3086 + "labelBlueskyModeration": "Bluesky moderation", 3087 + "@labelBlueskyModeration": { 3088 + "description": "Built-in Bluesky moderation labeler title" 3089 + }, 3090 + "labelBuiltIn": "Built-in", 3091 + "@labelBuiltIn": { 3092 + "description": "Chip label for a built-in item" 3093 + }, 3094 + "labelBuiltInLabeler": "Built-in labeler", 3095 + "@labelBuiltInLabeler": { 3096 + "description": "Moderation settings section title for built-in labeler" 3097 + }, 3098 + "labelBuiltInModeration": "Built-in moderation", 3099 + "@labelBuiltInModeration": { 3100 + "description": "Moderation labeler chip and switch label for built-in moderation" 3101 + }, 3102 + "labelBlockedAccount": "Blocked account", 3103 + "@labelBlockedAccount": { 3104 + "description": "Moderation badge for blocked account content" 3105 + }, 3106 + "labelBlockedByAccount": "Blocked by account", 3107 + "@labelBlockedByAccount": { 3108 + "description": "Moderation badge for content from an account that blocked the user" 3109 + }, 3110 + "labelBlockedRelationship": "Blocked relationship", 3111 + "@labelBlockedRelationship": { 3112 + "description": "Moderation badge for content limited by a block relationship" 3113 + }, 3114 + "labelCollections": "COLLECTIONS", 3115 + "@labelCollections": { 3116 + "description": "Devtools section heading for repository collections" 3117 + }, 3118 + "labelContentPreferenceHide": "Hide", 3119 + "@labelContentPreferenceHide": { 3120 + "description": "Moderation label preference option to hide matching content" 3121 + }, 3122 + "labelContentPreferenceIgnore": "Ignore", 3123 + "@labelContentPreferenceIgnore": { 3124 + "description": "Moderation label preference option to ignore matching content" 3125 + }, 3126 + "labelContentPreferenceWarn": "Warn", 3127 + "@labelContentPreferenceWarn": { 3128 + "description": "Moderation label preference option to warn on matching content" 3129 + }, 3130 + "labelCustomLabelers": "Custom labelers", 3131 + "@labelCustomLabelers": { 3132 + "description": "Moderation settings section title for custom labelers" 3133 + }, 3134 + "labelLabeler": "Labeler", 3135 + "@labelLabeler": { 3136 + "description": "Labeler detail screen app bar title" 3137 + }, 3138 + "labelLabelerDid": "Labeler DID", 3139 + "@labelLabelerDid": { 3140 + "description": "Input label for labeler DID" 3141 + }, 3142 + "labelLabelersAndContentModeration": "Labelers and content moderation", 3143 + "@labelLabelersAndContentModeration": { 3144 + "description": "Moderation settings hero title" 3145 + }, 3146 + "labelLabelPreferences": "Label preferences", 3147 + "@labelLabelPreferences": { 3148 + "description": "Labeler detail section heading for preferences" 3149 + }, 3150 + "labelHiddenContent": "Hidden content", 3151 + "@labelHiddenContent": { 3152 + "description": "Moderation badge for hidden content" 3153 + }, 3154 + "labelLogLevelDebug": "Debug", 3155 + "@labelLogLevelDebug": { 3156 + "description": "Log level filter chip for debug logs" 3157 + }, 3158 + "labelLogLevelError": "Error", 3159 + "@labelLogLevelError": { 3160 + "description": "Log level filter chip for error logs" 3161 + }, 3162 + "labelLogLevelFatal": "Fatal", 3163 + "@labelLogLevelFatal": { 3164 + "description": "Log level filter chip for fatal logs" 3165 + }, 3166 + "labelLogLevelInfo": "Info", 3167 + "@labelLogLevelInfo": { 3168 + "description": "Log level filter chip for info logs" 3169 + }, 3170 + "labelLogLevelTrace": "Trace", 3171 + "@labelLogLevelTrace": { 3172 + "description": "Log level filter chip for trace logs" 3173 + }, 3174 + "labelLogLevelWarning": "Warning", 3175 + "@labelLogLevelWarning": { 3176 + "description": "Log level filter chip for warning logs" 3177 + }, 3178 + "labelModerationNote": "Moderation note", 3179 + "@labelModerationNote": { 3180 + "description": "Fallback informational moderation badge label" 3181 + }, 3182 + "labelModerationSourceBluesky": "Bluesky", 3183 + "@labelModerationSourceBluesky": { 3184 + "description": "Moderation source label for the built-in Bluesky labeler" 3185 + }, 3186 + "labelModerationSourceSubscribedLabeler": "Subscribed labeler", 3187 + "@labelModerationSourceSubscribedLabeler": { 3188 + "description": "Moderation source label for a subscribed custom labeler" 3189 + }, 3190 + "labelMutedAccount": "Muted account", 3191 + "@labelMutedAccount": { 3192 + "description": "Moderation badge for muted account content" 3193 + }, 3194 + "labelMutedPhrase": "Muted phrase", 3195 + "@labelMutedPhrase": { 3196 + "description": "Moderation badge for muted phrase content" 3197 + }, 3198 + "labelNoCustomLabelDefinitions": "No custom label definitions", 3199 + "@labelNoCustomLabelDefinitions": { 3200 + "description": "Empty state title when a labeler has no localized custom definitions" 3201 + }, 3202 + "labelNoCustomLabelers": "No custom labelers", 3203 + "@labelNoCustomLabelers": { 3204 + "description": "Empty state title when no custom labelers are subscribed" 3205 + }, 3206 + "labelPdsExplorer": "PDS Explorer", 3207 + "@labelPdsExplorer": { 3208 + "description": "Developer PDS Explorer screen title" 3209 + }, 3210 + "labelPublishedPolicies": "Published policies", 3211 + "@labelPublishedPolicies": { 3212 + "description": "Labeler detail section heading for published policies" 3213 + }, 3214 + "labelRecordJson": "Record JSON", 3215 + "@labelRecordJson": { 3216 + "description": "Devtools breadcrumb label for an empty record key" 3217 + }, 3218 + "labelRefresh": "Refresh", 3219 + "@labelRefresh": { 3220 + "description": "Generic refresh tooltip label" 3221 + }, 3222 + "labelRepository": "Repository", 3223 + "@labelRepository": { 3224 + "description": "Devtools breadcrumb fallback label for repository" 3225 + }, 3226 + "labelSubscribed": "Subscribed", 3227 + "@labelSubscribed": { 3228 + "description": "Labeler detail switch label for subscribed labelers" 3229 + }, 3230 + "labelSensitiveContent": "Sensitive content", 3231 + "@labelSensitiveContent": { 3232 + "description": "Fallback moderation label for sensitive content" 3233 + }, 3234 + "labelUnknown": "Unknown", 3235 + "@labelUnknown": { 3236 + "description": "Generic fallback label when a name is unknown" 3237 + }, 3238 + "messageAddLabelerDidHelper": "Paste a labeler DID to review and subscribe to its labels.", 3239 + "@messageAddLabelerDidHelper": { 3240 + "description": "Helper text in add labeler dialog" 3241 + }, 3242 + "messageAdultContentRequiredForLabels": "Required before 18+ label preferences can be changed.", 3243 + "@messageAdultContentRequiredForLabels": { 3244 + "description": "Moderation settings adult content switch helper text" 3245 + }, 3246 + "messageBuiltInLabelerActiveWhenUnavailable": "The built-in labeler is active even if its details cannot be loaded right now.", 3247 + "@messageBuiltInLabelerActiveWhenUnavailable": { 3248 + "description": "Moderation settings fallback subtitle when built-in labeler details are unavailable" 3249 + }, 3250 + "messageBuiltInLabelerAlwaysActive": "This labeler is always active.", 3251 + "@messageBuiltInLabelerAlwaysActive": { 3252 + "description": "Labeler detail subtitle for built-in labeler subscription switch" 3253 + }, 3254 + "messageDevtoolsEmptyState": "Enter a handle, DID, or AT-URI to explore\na user''s repository.", 3255 + "@messageDevtoolsEmptyState": { 3256 + "description": "Developer PDS Explorer empty state helper text" 3257 + }, 3258 + "messageEnableAdultContentForLabel": "Enable adult content to change this 18+ label.", 3259 + "@messageEnableAdultContentForLabel": { 3260 + "description": "Helper text shown when adult content is required to edit a label preference" 3261 + }, 3262 + "messageBlockedAccountDescription": "This account is blocked", 3263 + "@messageBlockedAccountDescription": { 3264 + "description": "Tooltip for blocked account moderation badge" 3265 + }, 3266 + "messageBlockedByAccountDescription": "This account has blocked you", 3267 + "@messageBlockedByAccountDescription": { 3268 + "description": "Tooltip for blocked-by account moderation badge" 3269 + }, 3270 + "messageBlockedRelationshipDescription": "This content is limited by a block relationship", 3271 + "@messageBlockedRelationshipDescription": { 3272 + "description": "Tooltip for blocked relationship moderation badge" 3273 + }, 3274 + "messageHiddenContentDescription": "This content is hidden by moderation rules", 3275 + "@messageHiddenContentDescription": { 3276 + "description": "Tooltip for hidden content moderation badge" 3277 + }, 3278 + "messageJsonCopiedToClipboard": "JSON copied to clipboard", 3279 + "@messageJsonCopiedToClipboard": { 3280 + "description": "Snackbar after copying record JSON" 3281 + }, 3282 + "messageLogsEmpty": "No logs yet", 3283 + "@messageLogsEmpty": { 3284 + "description": "Log viewer empty state title" 3285 + }, 3286 + "messageLogsEmptySubtitle": "Log entries will appear here", 3287 + "@messageLogsEmptySubtitle": { 3288 + "description": "Log viewer empty state subtitle" 3289 + }, 3290 + "messageModerationSettingsHeroSubtitle": "Manage adult-content visibility, subscribed labelers, and the rules each labeler applies to posts and profiles.", 3291 + "@messageModerationSettingsHeroSubtitle": { 3292 + "description": "Moderation settings hero subtitle" 3293 + }, 3294 + "messageModerationGuidanceApplies": "Moderation guidance applies here", 3295 + "@messageModerationGuidanceApplies": { 3296 + "description": "Fallback moderation badge tooltip" 3297 + }, 3298 + "messageMutedAccountDescription": "Muted content is being downranked here", 3299 + "@messageMutedAccountDescription": { 3300 + "description": "Tooltip for muted account moderation badge" 3301 + }, 3302 + "messageMutedPhraseDescription": "A muted phrase matched this content", 3303 + "@messageMutedPhraseDescription": { 3304 + "description": "Tooltip for muted phrase moderation badge" 3305 + }, 3306 + "messageNoCustomLabelDefinitions": "This labeler publishes values, but not localized custom definitions.", 3307 + "@messageNoCustomLabelDefinitions": { 3308 + "description": "Labeler detail empty state subtitle when no localized custom definitions are available" 3309 + }, 3310 + "messageNoCustomLabelers": "Add a labeler DID to subscribe and configure its custom labels.", 3311 + "@messageNoCustomLabelers": { 3312 + "description": "Moderation settings empty state subtitle when no custom labelers are subscribed" 3313 + }, 3314 + "messageNoLabelDescriptionAvailable": "No description available for this label.", 3315 + "@messageNoLabelDescriptionAvailable": { 3316 + "description": "Fallback description for a label definition without localized description" 3317 + }, 3318 + "messageNoLogFileAvailable": "No log file available", 3319 + "@messageNoLogFileAvailable": { 3320 + "description": "Snackbar when there is no log file to share" 3321 + }, 3322 + "messageRecordCountsUnavailable": "Record counts unavailable", 3323 + "@messageRecordCountsUnavailable": { 3324 + "description": "Devtools repository count status when record counts cannot be loaded" 3325 + }, 3326 + "messageRecordCountsLoading": "Counting records...", 3327 + "@messageRecordCountsLoading": { 3328 + "description": "Devtools repository count status while record counts are loading" 3329 + }, 3330 + "messageSubscribedLabelersHeaders": "Subscribed labelers are added to your moderation headers and preferences.", 3331 + "@messageSubscribedLabelersHeaders": { 3332 + "description": "Labeler detail subtitle for custom labeler subscription switch" 3333 + }, 3334 + "messageUnableToOpenShareSheet": "Unable to open share sheet. Please try again.", 3335 + "@messageUnableToOpenShareSheet": { 3336 + "description": "Snackbar when the log share sheet cannot open" 3337 + }, 3338 + "placeholderHandleDidOrAtUri": "Handle, DID, or at:// URI", 3339 + "@placeholderHandleDidOrAtUri": { 3340 + "description": "Devtools search input placeholder" 3341 + }, 3342 + "placeholderLabelerDid": "did:plc:examplelabeler", 3343 + "@placeholderLabelerDid": { 3344 + "description": "Placeholder DID in add labeler dialog" 3345 + }, 3346 + "placeholderLogsFilter": "Filter logs...", 3347 + "@placeholderLogsFilter": { 3348 + "description": "Log viewer search field placeholder" 3349 + }, 3350 + "subjectLazuriteLogs": "Lazurite logs", 3351 + "@subjectLazuriteLogs": { 3352 + "description": "Share sheet subject for log files" 3353 + }, 3354 + "tooltipClearAllLogs": "Clear all logs", 3355 + "@tooltipClearAllLogs": { 3356 + "description": "Tooltip for clearing all log files" 3357 + }, 3358 + "tooltipGoToPdsLs": "Go to pds.ls", 3359 + "@tooltipGoToPdsLs": { 3360 + "description": "Tooltip for opening pds.ls" 3361 + }, 3362 + "tooltipShareLogFile": "Share log file", 3363 + "@tooltipShareLogFile": { 3364 + "description": "Tooltip for sharing a log file" 775 3365 } 776 3366 }
+43 -16
lib/features/account/presentation/account_switcher_sheet.dart
··· 4 4 import 'package:flutter_bloc/flutter_bloc.dart'; 5 5 import 'package:go_router/go_router.dart'; 6 6 import 'package:lazurite/core/database/app_database.dart'; 7 + import 'package:lazurite/core/l10n/l10n.dart'; 7 8 import 'package:lazurite/features/account/cubit/account_switcher_cubit.dart'; 8 9 import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 9 10 import 'package:lazurite/features/auth/data/atproto_identifier.dart'; ··· 26 27 return code.message; 27 28 } 28 29 30 + String? validateAtProtoIdentifierInputLocalized(BuildContext context, String? value) { 31 + final normalized = normalizeAtProtoIdentifierForAuth(value ?? ''); 32 + final validationError = validateAtProtoIdentifierForAuth(normalized); 33 + if (validationError == null) { 34 + return null; 35 + } 36 + 37 + return _localizedAtProtoIdentifierValidationMessage(context, validationError.code); 38 + } 39 + 40 + String _localizedAtProtoIdentifierValidationMessage(BuildContext context, AtProtoIdentifierValidationErrorCode code) { 41 + return switch (code) { 42 + AtProtoIdentifierValidationErrorCode.empty => context.l10n.validationEnterBlueskyHandleOrDid, 43 + AtProtoIdentifierValidationErrorCode.unsupportedDid => context.l10n.validationUseSupportedDid, 44 + AtProtoIdentifierValidationErrorCode.invalidDid => context.l10n.validationEnterCompleteDid, 45 + AtProtoIdentifierValidationErrorCode.invalidHandle => context.l10n.validationEnterFullHandle, 46 + }; 47 + } 48 + 29 49 void showAccountSwitcherSheet(BuildContext context) { 30 50 final cubit = context.read<AccountSwitcherCubit>(); 31 51 final authBloc = context.read<AuthBloc>(); ··· 67 87 children: [ 68 88 Padding( 69 89 padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), 70 - child: Text('Accounts', style: textTheme.titleMedium), 90 + child: Text(context.l10n.labelAccounts, style: textTheme.titleMedium), 71 91 ), 72 92 const Divider(), 73 93 BlocBuilder<AccountSwitcherCubit, AccountSwitcherState>( ··· 79 99 ); 80 100 } 81 101 82 - if (state.accounts.isEmpty) return _buildEmptyState(colorScheme, textTheme); 102 + if (state.accounts.isEmpty) return _buildEmptyState(context, colorScheme, textTheme); 83 103 84 104 return ListView.builder( 85 105 shrinkWrap: true, ··· 99 119 children: [ 100 120 if (isActive) const Icon(Icons.check), 101 121 IconButton( 102 - tooltip: 'Remove account', 122 + tooltip: context.l10n.labelRemoveAccount, 103 123 icon: const Icon(Icons.delete_outline), 104 124 onPressed: () => _onRemoveAccount(context, account.did, account.handle), 105 125 ), ··· 114 134 const Divider(), 115 135 ListTile( 116 136 leading: const Icon(Icons.person_add_outlined), 117 - title: const Text('Add Account'), 137 + title: Text(context.l10n.buttonAddAccount), 118 138 onTap: () => _onAddAccount(context), 119 139 ), 120 140 ], ··· 122 142 ); 123 143 } 124 144 125 - Widget _buildEmptyState(ColorScheme colorScheme, TextTheme textTheme) => Padding( 145 + Widget _buildEmptyState(BuildContext context, ColorScheme colorScheme, TextTheme textTheme) => Padding( 126 146 padding: const EdgeInsets.fromLTRB(24, 20, 24, 24), 127 147 child: Row( 128 148 children: [ ··· 130 150 const SizedBox(width: 12), 131 151 Expanded( 132 152 child: Text( 133 - 'No other signed-in accounts yet. Add an account to switch between profiles.', 153 + context.l10n.accountSwitcherNoOtherAccounts, 134 154 style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), 135 155 ), 136 156 ), ··· 148 168 } 149 169 150 170 if (parentContext.mounted) { 151 - showAppSnackBar(parentContext, 'Please sign in again for that account.'); 171 + showAppSnackBar(parentContext, parentContext.l10n.messagePleaseSignInAgainForAccount); 152 172 final router = GoRouter.maybeOf(parentContext); 153 173 if (router != null) { 154 174 unawaited(Future<void>.delayed(Duration.zero, () => router.go(_reauthLoginLocation(account.handle)))); ··· 170 190 context: parentContext, 171 191 builder: (dialogContext) => StatefulBuilder( 172 192 builder: (dialogContext, setDialogState) => ConfirmationDialog( 173 - title: const Text('Add Account'), 193 + title: Text(parentContext.l10n.buttonAddAccount), 174 194 content: SizedBox( 175 195 width: 420, 176 196 child: Form( ··· 186 206 minChars: 2, 187 207 debounceMs: 300, 188 208 limit: 8, 189 - decoration: const InputDecoration(labelText: 'Handle or DID', hintText: 'username.bsky.social'), 209 + decoration: InputDecoration( 210 + labelText: parentContext.l10n.promptHandleOrDid, 211 + hintText: parentContext.l10n.placeholderUsernameBskySocial, 212 + ), 190 213 textInputAction: TextInputAction.done, 191 - validator: validateAtProtoIdentifierInput, 214 + validator: (value) => validateAtProtoIdentifierInputLocalized(parentContext, value), 192 215 onChanged: (_) => setDialogState(() {}), 193 216 onFieldSubmitted: (_) { 194 217 if ((formKey.currentState?.validate() ?? false)) { ··· 199 222 ), 200 223 ), 201 224 confirmEnabled: _isIdentifierInputValid(controller.text.trim()), 202 - confirmLabel: 'Continue', 225 + confirmLabel: parentContext.l10n.buttonContinue, 203 226 onCancel: () => Navigator.pop(dialogContext), 204 227 onConfirm: () { 205 228 if (!(formKey.currentState?.validate() ?? false)) { ··· 219 242 if (tokens != null) { 220 243 authBloc.add(SessionRestored(tokens: tokens)); 221 244 } else if (parentContext.mounted) { 222 - showAppSnackBar(parentContext, cubit.lastAddAccountErrorMessage ?? 'Failed to add account', isError: true); 245 + showAppSnackBar( 246 + parentContext, 247 + cubit.lastAddAccountErrorMessage ?? parentContext.l10n.errorFailedToAddAccount, 248 + isError: true, 249 + ); 223 250 } 224 251 } 225 252 ··· 228 255 final remove = await showDialog<bool>( 229 256 context: parentContext, 230 257 builder: (dialogContext) => ConfirmationDialog( 231 - title: const Text('Remove Account'), 232 - content: Text('Remove @$handle from this device?'), 233 - confirmLabel: 'Remove', 258 + title: Text(parentContext.l10n.dialogRemoveAccountTitle), 259 + content: Text(parentContext.l10n.formatRemoveAccountContent(handle)), 260 + confirmLabel: parentContext.l10n.buttonRemove, 234 261 onCancel: () => Navigator.pop(dialogContext, false), 235 262 onConfirm: () => Navigator.pop(dialogContext, true), 236 263 ), ··· 243 270 final result = await cubit.removeAccount(did); 244 271 if (!result.removed) { 245 272 if (parentContext.mounted) { 246 - showAppSnackBar(parentContext, 'Unable to remove account right now.', isError: true); 273 + showAppSnackBar(parentContext, parentContext.l10n.errorUnableToRemoveAccountNow, isError: true); 247 274 } 248 275 return; 249 276 }
+6 -5
lib/features/alerts/presentation/alerts_screen.dart
··· 2 2 import 'package:bluesky/chat_bsky_convo_defs.dart'; 3 3 import 'package:flutter_bloc/flutter_bloc.dart'; 4 4 import 'package:go_router/go_router.dart'; 5 + import 'package:lazurite/core/l10n/l10n.dart'; 5 6 import 'package:lazurite/core/widgets/lazurite_app_bar.dart'; 6 7 import 'package:lazurite/features/messages/bloc/convo_list_bloc.dart'; 7 8 import 'package:lazurite/features/messages/presentation/widgets/convo_list_pane.dart'; ··· 40 41 return AppScreenEntrance( 41 42 child: Scaffold( 42 43 appBar: LazuriteAppBar( 43 - sectionLabel: 'Alerts', 44 + sectionLabel: context.l10n.labelAlertsTitle, 44 45 actions: currentTab == AlertsTab.notifications 45 - ? [TextButton(onPressed: () => _markAllRead(context), child: const Text('Mark All Read'))] 46 + ? [TextButton(onPressed: () => _markAllRead(context), child: Text(context.l10n.buttonMarkAllRead))] 46 47 : null, 47 48 bottom: PreferredSize( 48 49 preferredSize: const Size.fromHeight(48), ··· 108 109 children: [ 109 110 _AlertsTabButton( 110 111 tab: AlertsTab.notifications, 111 - label: 'Notifications', 112 + label: context.l10n.labelNotifications, 112 113 currentTab: currentTab, 113 114 unreadCount: notificationsUnreadCount, 114 115 ), 115 116 _AlertsTabButton( 116 117 tab: AlertsTab.messages, 117 - label: 'Messages', 118 + label: context.l10n.labelMessages, 118 119 currentTab: currentTab, 119 120 unreadCount: messagesUnreadCount, 120 121 ), 121 - _AlertsTabButton(tab: AlertsTab.requests, label: 'Requests', currentTab: currentTab), 122 + _AlertsTabButton(tab: AlertsTab.requests, label: context.l10n.labelMessageRequests, currentTab: currentTab), 122 123 ], 123 124 ), 124 125 );
+202 -121
lib/features/compose/presentation/compose_screen.dart
··· 10 10 import 'package:image_picker/image_picker.dart'; 11 11 import 'package:intl/intl.dart'; 12 12 import 'package:lazurite/core/database/app_database.dart'; 13 + import 'package:lazurite/core/l10n/l10n.dart'; 13 14 import 'package:lazurite/core/logging/app_logger.dart'; 14 15 import 'package:lazurite/core/theme/theme_extensions.dart'; 15 16 import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 16 17 import 'package:lazurite/features/compose/bloc/compose_bloc.dart'; 17 18 import 'package:lazurite/features/compose/data/link_preview_service.dart'; 18 - import 'package:lazurite/features/connectivity/connectivity_helpers.dart'; 19 19 import 'package:lazurite/features/connectivity/cubit/connectivity_cubit.dart'; 20 20 import 'package:lazurite/features/profile/data/profile_repository.dart'; 21 21 import 'package:lazurite/features/typeahead/data/typeahead_repository.dart'; ··· 24 24 import 'package:lazurite/shared/presentation/widgets/confirmation_dialog.dart'; 25 25 import 'package:lazurite/shared/presentation/widgets/external_link_preview_card.dart'; 26 26 import 'package:lazurite/shared/presentation/widgets/profile_avatar.dart'; 27 + import 'package:lazurite/shared/utils/format_utils.dart'; 27 28 import 'package:video_player/video_player.dart'; 28 29 29 30 class ComposeScreen extends StatefulWidget { ··· 407 408 final state = context.read<ComposeBloc>().state; 408 409 if (!state.canAddMoreMedia) { 409 410 if (mounted) { 410 - showAppSnackBar(context, 'Maximum 4 images allowed', isError: true); 411 + showAppSnackBar(context, context.l10n.messageComposeImageMaxCount, isError: true); 411 412 } 412 413 return; 413 414 } ··· 426 427 const maxSize = 1 * 1024 * 1024; 427 428 if (fileSize > maxSize) { 428 429 if (mounted) { 429 - showAppSnackBar(context, 'Image must be smaller than 1MB', isError: true); 430 + showAppSnackBar(context, context.l10n.messageComposeImageMustBeUnder1Mb, isError: true); 430 431 } 431 432 return; 432 433 } ··· 435 436 const validExtensions = ['jpg', 'jpeg', 'png', 'webp']; 436 437 if (!validExtensions.contains(extension)) { 437 438 if (mounted) { 438 - showAppSnackBar(context, 'Image must be JPEG, PNG, or WebP', isError: true); 439 + showAppSnackBar(context, context.l10n.messageComposeImageMustBeJpegPngWebp, isError: true); 439 440 } 440 441 return; 441 442 } ··· 452 453 } 453 454 } catch (e) { 454 455 if (mounted) { 455 - showAppSnackBar(context, 'Failed to pick image: $e', isError: true); 456 + showAppSnackBar(context, context.l10n.formatComposeFailedToPickImage(e), isError: true); 456 457 } 457 458 } 458 459 } ··· 461 462 final state = context.read<ComposeBloc>().state; 462 463 if (!state.canAddVideo) { 463 464 if (mounted) { 464 - showAppSnackBar(context, 'Remove existing media before adding a video', isError: true); 465 + showAppSnackBar(context, context.l10n.messageComposeRemoveExistingMediaBeforeVideo, isError: true); 465 466 } 466 467 return; 467 468 } ··· 473 474 } 474 475 } catch (e) { 475 476 if (mounted) { 476 - showAppSnackBar(context, 'Failed to pick video: $e', isError: true); 477 + showAppSnackBar(context, context.l10n.formatComposeFailedToPickVideo(e), isError: true); 477 478 } 478 479 } 479 480 } ··· 544 545 } 545 546 } 546 547 547 - String _formatDraftTime(DateTime dateTime) { 548 - final now = DateTime.now(); 549 - final difference = now.difference(dateTime); 550 - 551 - if (difference.isNegative) { 552 - return 'Just now'; 553 - } 554 - 555 - if (difference.inMinutes < 1) { 556 - return 'Just now'; 557 - } else if (difference.inMinutes < 60) { 558 - return '${difference.inMinutes}m ago'; 559 - } else if (difference.inHours < 24) { 560 - return '${difference.inHours}h ago'; 561 - } else if (difference.inDays < 7) { 562 - return '${difference.inDays}d ago'; 563 - } else { 564 - return DateFormat('MMM d').format(dateTime); 565 - } 548 + String _formatDraftTime(BuildContext context, DateTime dateTime) { 549 + return formatRelativeTime( 550 + dateTime, 551 + nowLabel: context.l10n.commonJustNow, 552 + includeAgo: true, 553 + locale: Localizations.localeOf(context).toString(), 554 + ); 566 555 } 567 556 568 - String _videoStatusLabel(VideoAttachment video) { 557 + String _videoStatusLabel(BuildContext context, VideoAttachment video) { 569 558 return switch (video.status) { 570 - VideoUploadStatus.idle => 'Ready to upload', 571 - VideoUploadStatus.checkingLimits => 'Checking upload limits…', 572 - VideoUploadStatus.uploading => video.uploadProgress > 0 ? 'Uploading… ${video.uploadProgress}%' : 'Uploading…', 573 - VideoUploadStatus.processing => video.uploadProgress > 0 ? 'Processing… ${video.uploadProgress}%' : 'Processing…', 574 - VideoUploadStatus.ready => video.altText.isNotEmpty ? 'Ready · "${video.altText}"' : 'Ready', 575 - VideoUploadStatus.error => video.errorMessage ?? 'Upload failed', 559 + VideoUploadStatus.idle => context.l10n.messageVideoReadyToUpload, 560 + VideoUploadStatus.checkingLimits => context.l10n.messageVideoCheckingUploadLimits, 561 + VideoUploadStatus.uploading => 562 + video.uploadProgress > 0 563 + ? '${context.l10n.messageVideoUploading} ${video.uploadProgress}%' 564 + : context.l10n.messageVideoUploading, 565 + VideoUploadStatus.processing => 566 + video.uploadProgress > 0 567 + ? '${context.l10n.messageVideoProcessing} ${video.uploadProgress}%' 568 + : context.l10n.messageVideoProcessing, 569 + VideoUploadStatus.ready => 570 + video.altText.isNotEmpty 571 + ? context.l10n.formatComposeVideoReadyWithAltText(video.altText) 572 + : context.l10n.messageVideoReady, 573 + VideoUploadStatus.error => _localizedComposeError( 574 + context, 575 + video.errorMessage ?? context.l10n.messageVideoUploadFailed, 576 + ), 576 577 }; 577 578 } 578 579 ··· 607 608 child: Row( 608 609 mainAxisAlignment: MainAxisAlignment.spaceBetween, 609 610 children: [ 610 - Text('Drafts', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)), 611 + Text( 612 + context.l10n.messageComposeDrafts, 613 + style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), 614 + ), 611 615 Text( 612 - '${state.drafts.length} draft${state.drafts.length == 1 ? '' : 's'}', 616 + context.l10n.formatDraftCount(state.drafts.length), 613 617 style: theme.textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), 614 618 ), 615 619 ], ··· 625 629 padding: const EdgeInsets.symmetric(vertical: 26), 626 630 child: Center( 627 631 child: Text( 628 - 'No drafts saved', 632 + context.l10n.messageComposeNoDraftsSaved, 629 633 style: theme.textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), 630 634 ), 631 635 ), ··· 640 644 final draft = state.drafts[index]; 641 645 return _DraftListItem( 642 646 draft: draft, 643 - formattedTime: _formatDraftTime(draft.updatedAt), 647 + formattedTime: _formatDraftTime(context, draft.updatedAt), 644 648 onTap: () { 645 649 setState(() => _showDrafts = false); 646 650 context.read<ComposeBloc>().add(DraftLoaded(draft.id)); ··· 649 653 final bloc = context.read<ComposeBloc>(); 650 654 showConfirmationDialog( 651 655 context: context, 652 - title: const Text('Delete Draft?'), 653 - content: const Text('This action cannot be undone.'), 654 - confirmLabel: 'Delete', 656 + title: Text(context.l10n.dialogDeleteDraftTitle), 657 + content: Text(context.l10n.dialogDeletePostContent), 658 + confirmLabel: context.l10n.buttonDelete, 655 659 confirmDestructive: true, 656 660 ).then((confirmed) { 657 661 if (confirmed && mounted) { ··· 748 752 crossAxisAlignment: CrossAxisAlignment.start, 749 753 children: [ 750 754 Text( 751 - quotedHandle != null && quotedHandle.isNotEmpty ? 'Quoting @$quotedHandle' : 'Quoting post', 755 + quotedHandle != null && quotedHandle.isNotEmpty 756 + ? context.l10n.formatComposeQuotingHandle(quotedHandle) 757 + : context.l10n.messageComposeQuotingPost, 752 758 style: theme.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600), 753 759 ), 754 760 if (quotedText.isNotEmpty) ...[ ··· 767 773 onPressed: () => context.read<ComposeBloc>().add(const QuoteContextCleared()), 768 774 icon: const Icon(Icons.close), 769 775 visualDensity: VisualDensity.compact, 770 - tooltip: 'Remove quoted post', 776 + tooltip: context.l10n.messageComposeRemoveQuotedPost, 771 777 ), 772 778 ], 773 779 ), ··· 807 813 if (context.read<ComposeBloc>().state.isEditing) return; 808 814 context.read<ComposeBloc>().add(const DraftSaved()); 809 815 if (mounted) { 810 - showAppSnackBar(context, 'Draft saved'); 816 + showAppSnackBar(context, context.l10n.messageComposeDraftSaved); 811 817 } 812 818 } 813 819 814 820 Future<void> _showEditAlgorithmInfo() async { 815 821 await showConfirmationDialog( 816 822 context: context, 817 - title: const Text('How Post Editing Works'), 818 - content: const Text( 819 - 'Lazurite saves edits by deleting and recreating the post record with the same URI. During re-indexing, ' 820 - 'ranking, counters, and search visibility can shift, and updates may take time to appear everywhere.', 821 - ), 822 - confirmLabel: 'OK', 823 + title: Text(context.l10n.dialogEditAlgorithmTitle), 824 + content: Text(context.l10n.dialogEditAlgorithmContent), 825 + confirmLabel: context.l10n.buttonOk, 823 826 showCancel: false, 824 827 ); 825 828 } ··· 834 837 if (state.isDraftDirty) { 835 838 showConfirmationDialog( 836 839 context: context, 837 - title: const Text('Discard Changes?'), 838 - content: const Text('You have unsaved edits. Discard them and leave?'), 839 - confirmLabel: 'Discard', 840 + title: Text(context.l10n.dialogDiscardChangesTitle), 841 + content: Text(context.l10n.dialogDiscardChangesContent), 842 + confirmLabel: context.l10n.buttonDiscard, 840 843 ).then((shouldDiscard) { 841 844 if (shouldDiscard && mounted) { 842 845 navigator.pop(false); ··· 851 854 if (hasContent && state.isDraftDirty) { 852 855 showConfirmationDialog( 853 856 context: context, 854 - title: const Text('Save Draft?'), 855 - content: const Text('You have unsaved content. Would you like to save it as a draft?'), 856 - cancelLabel: 'Discard', 857 - confirmLabel: 'Save', 857 + title: Text(context.l10n.dialogSaveDraftTitle), 858 + content: Text(context.l10n.dialogSaveDraftContent), 859 + cancelLabel: context.l10n.buttonDiscard, 860 + confirmLabel: context.l10n.buttonSave, 858 861 ).then((shouldSave) { 859 862 if (shouldSave) { 860 863 _saveDraft(); ··· 887 890 } 888 891 } 889 892 890 - return 'Lazurite'; 893 + return context.l10n.appTitle; 891 894 } 892 895 893 896 Widget _buildComposerAvatar() { ··· 926 929 ); 927 930 } 928 931 929 - Widget _buildComposerTextArea() { 930 - return Expanded( 931 - child: Padding( 932 - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), 933 - child: Row( 934 - crossAxisAlignment: CrossAxisAlignment.start, 935 - children: [ 936 - _buildComposerAvatar(), 937 - const SizedBox(width: 12), 938 - Expanded( 939 - child: TextField( 940 - controller: _textController, 941 - focusNode: _textFocusNode, 942 - autofocus: true, 943 - maxLines: null, 944 - expands: true, 945 - textAlignVertical: TextAlignVertical.top, 946 - decoration: InputDecoration( 947 - hintText: "What's on your mind?", 948 - hintStyle: theme.textTheme.bodyLarge?.copyWith(color: theme.colorScheme.onSurfaceVariant), 949 - border: InputBorder.none, 950 - contentPadding: EdgeInsets.zero, 951 - ), 952 - style: theme.textTheme.bodyLarge?.copyWith(height: 1.5, fontSize: 16), 932 + Widget _buildComposerTextArea() => Expanded( 933 + child: Padding( 934 + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), 935 + child: Row( 936 + crossAxisAlignment: CrossAxisAlignment.start, 937 + children: [ 938 + _buildComposerAvatar(), 939 + const SizedBox(width: 12), 940 + Expanded( 941 + child: TextField( 942 + controller: _textController, 943 + focusNode: _textFocusNode, 944 + autofocus: true, 945 + maxLines: null, 946 + expands: true, 947 + textAlignVertical: TextAlignVertical.top, 948 + decoration: InputDecoration( 949 + hintText: context.l10n.messageComposePlaceholder, 950 + hintStyle: theme.textTheme.bodyLarge?.copyWith(color: theme.colorScheme.onSurfaceVariant), 951 + border: InputBorder.none, 952 + contentPadding: EdgeInsets.zero, 953 953 ), 954 + style: theme.textTheme.bodyLarge?.copyWith(height: 1.5, fontSize: 16), 954 955 ), 955 - ], 956 - ), 956 + ), 957 + ], 957 958 ), 958 - ); 959 - } 959 + ), 960 + ); 960 961 961 962 Widget _buildScheduledPill(ComposeState state) { 962 963 if (!state.hasScheduledTime) { ··· 980 981 Icon(Icons.schedule, size: 15, color: colorScheme.onPrimary), 981 982 const SizedBox(width: 7), 982 983 Text( 983 - 'Scheduled for ${DateFormat('MMM d, h:mm a').format(state.scheduledAt!)}', 984 + context.l10n.formatComposeScheduledFor( 985 + DateFormat.yMMMd(Localizations.localeOf(context).toString()).add_jm().format(state.scheduledAt!), 986 + ), 984 987 style: theme.textTheme.bodySmall?.copyWith(color: colorScheme.onPrimary, fontWeight: FontWeight.w600), 985 988 ), 986 989 const SizedBox(width: 2), 987 990 IconButton( 988 991 onPressed: () => context.read<ComposeBloc>().add(const ScheduleCleared()), 989 992 icon: Icon(Icons.close, size: 16, color: colorScheme.onPrimary), 990 - tooltip: 'Clear scheduled time', 993 + tooltip: context.l10n.messageComposeClearScheduledTime, 991 994 constraints: const BoxConstraints(minWidth: 40, minHeight: 40), 992 995 padding: EdgeInsets.zero, 993 996 visualDensity: VisualDensity.compact, ··· 1038 1041 1039 1042 if (state.isSuccess) { 1040 1043 if (state.isEditing) { 1041 - showAppSnackBar(context, 'Changes saved.', behavior: SnackBarBehavior.floating); 1044 + showAppSnackBar(context, context.l10n.messageChangesSaved, behavior: SnackBarBehavior.floating); 1042 1045 } 1043 1046 Navigator.of(context).pop( 1044 1047 state.isEditing ··· 1053 1056 } 1054 1057 1055 1058 if (state.hasError && state.errorMessage != null) { 1056 - showAppSnackBar(context, state.errorMessage!, behavior: SnackBarBehavior.floating, isError: true); 1059 + showAppSnackBar( 1060 + context, 1061 + _localizedComposeError(context, state.errorMessage!), 1062 + behavior: SnackBarBehavior.floating, 1063 + isError: true, 1064 + ); 1057 1065 } 1058 1066 }, 1059 1067 child: PopScope( ··· 1069 1077 backgroundColor: theme.colorScheme.surface, 1070 1078 surfaceTintColor: Colors.transparent, 1071 1079 shape: Border(bottom: BorderSide(color: theme.colorScheme.outlineVariant)), 1072 - leading: TextButton(onPressed: () => _handleBackNavigation(context), child: const Text('Cancel')), 1080 + leading: TextButton( 1081 + onPressed: () => _handleBackNavigation(context), 1082 + child: Text(context.l10n.buttonCancel), 1083 + ), 1073 1084 leadingWidth: 80, 1074 1085 title: BlocBuilder<ComposeBloc, ComposeState>( 1075 1086 builder: (context, state) => Text( 1076 - state.isEditing ? 'Edit Post' : 'New Post', 1087 + state.isEditing ? context.l10n.labelEditPost : context.l10n.labelNewPost, 1077 1088 style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), 1078 1089 ), 1079 1090 ), ··· 1091 1102 ), 1092 1103 child: state.isSubmitting 1093 1104 ? const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)) 1094 - : Text(state.isEditing ? 'Save Changes' : 'Post'), 1105 + : Text(state.isEditing ? context.l10n.buttonSaveChanges : context.l10n.buttonPost), 1095 1106 ); 1096 1107 1097 1108 return Padding( 1098 1109 padding: const EdgeInsets.only(right: 8), 1099 1110 child: isOffline 1100 - ? Tooltip(message: offlineActionMessage('publish your post'), child: button) 1111 + ? Tooltip( 1112 + message: context.l10n.formatOfflineReconnectAction(context.l10n.actionPublishYourPost), 1113 + child: button, 1114 + ) 1101 1115 : button, 1102 1116 ); 1103 1117 }, ··· 1124 1138 const SizedBox(width: 12), 1125 1139 Expanded( 1126 1140 child: Text( 1127 - 'Edits are saved by replacing the record while keeping this post URI. Ranking, ' 1128 - 'counts, and visibility may shift while networks re-index.', 1141 + context.l10n.messageComposeEditNotice, 1129 1142 style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant), 1130 1143 ), 1131 1144 ), 1132 1145 IconButton( 1133 1146 onPressed: _showEditAlgorithmInfo, 1134 1147 icon: const Icon(Icons.help_outline), 1135 - tooltip: 'More info', 1148 + tooltip: context.l10n.labelMoreInfo, 1136 1149 ), 1137 1150 ], 1138 1151 ), ··· 1155 1168 Icon(Icons.reply, size: 16, color: theme.colorScheme.onSurfaceVariant), 1156 1169 const SizedBox(width: 8), 1157 1170 Text( 1158 - 'Replying to ', 1171 + '${context.l10n.messageReplyingTo} ', 1159 1172 style: Theme.of( 1160 1173 context, 1161 1174 ).textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant), ··· 1237 1250 constraints: const BoxConstraints(minWidth: 40, minHeight: 30), 1238 1251 child: Center( 1239 1252 child: Text( 1240 - 'ALT', 1253 + context.l10n.labelAlt, 1241 1254 style: theme.textTheme.labelSmall?.copyWith( 1242 1255 color: attachment.altText.isNotEmpty 1243 1256 ? theme.colorScheme.onPrimary ··· 1261 1274 style: IconButton.styleFrom(backgroundColor: Colors.black54), 1262 1275 onPressed: () => context.read<ComposeBloc>().add(MediaRemoved(index)), 1263 1276 icon: const Icon(Icons.close, size: 16, color: Colors.white), 1264 - tooltip: 'Remove image', 1277 + tooltip: context.l10n.messageComposeRemoveImage, 1265 1278 ), 1266 1279 ), 1267 1280 ), ··· 1327 1340 ), 1328 1341 const SizedBox(height: 2), 1329 1342 Text( 1330 - _videoStatusLabel(video), 1343 + _videoStatusLabel(context, video), 1331 1344 style: theme.textTheme.bodySmall?.copyWith( 1332 1345 color: video.hasError 1333 1346 ? theme.colorScheme.error ··· 1349 1362 if (video.isReady) ...[ 1350 1363 IconButton( 1351 1364 icon: const Icon(Icons.subtitles_outlined), 1352 - tooltip: 'Add alt text', 1365 + tooltip: context.l10n.messageComposeAddAltText, 1353 1366 onPressed: () => _showVideoAltTextDialog(video), 1354 1367 color: video.altText.isNotEmpty 1355 1368 ? theme.colorScheme.primary ··· 1360 1373 icon: const Icon(Icons.close), 1361 1374 onPressed: () => context.read<ComposeBloc>().add(const VideoRemoved()), 1362 1375 color: theme.colorScheme.onSurfaceVariant, 1376 + tooltip: context.l10n.buttonRemove, 1363 1377 ), 1364 1378 ], 1365 1379 ), ··· 1388 1402 if (state.isEditing) return const SizedBox.shrink(); 1389 1403 return _buildToolbarIconButton( 1390 1404 icon: Icons.image_outlined, 1391 - tooltip: 'Add image', 1405 + tooltip: context.l10n.messageComposeAddImage, 1392 1406 onPressed: state.canAddMoreMedia ? _pickImage : null, 1393 1407 ); 1394 1408 }, ··· 1398 1412 if (state.isEditing) return const SizedBox.shrink(); 1399 1413 return _buildToolbarIconButton( 1400 1414 icon: Icons.videocam_outlined, 1401 - tooltip: 'Add video', 1415 + tooltip: context.l10n.messageComposeAddVideo, 1402 1416 onPressed: state.canAddVideo ? _pickVideo : null, 1403 1417 ); 1404 1418 }, ··· 1413 1427 state.hasScheduledTime; 1414 1428 return _buildToolbarIconButton( 1415 1429 icon: Icons.save_outlined, 1416 - tooltip: 'Save draft', 1430 + tooltip: context.l10n.messageComposeSaveDraft, 1417 1431 onPressed: hasDraftableContent ? _saveDraft : null, 1418 1432 ); 1419 1433 }, ··· 1423 1437 if (state.isEditing) return const SizedBox.shrink(); 1424 1438 return _buildToolbarIconButton( 1425 1439 icon: Icons.drive_file_rename_outline, 1426 - tooltip: 'Drafts', 1440 + tooltip: context.l10n.messageComposeDrafts, 1427 1441 onPressed: _toggleDrafts, 1428 1442 isActive: _showDrafts, 1429 1443 ); ··· 1434 1448 if (state.isEditing) return const SizedBox.shrink(); 1435 1449 return _buildToolbarIconButton( 1436 1450 icon: Icons.schedule, 1437 - tooltip: 'Schedule', 1451 + tooltip: context.l10n.labelSchedule, 1438 1452 onPressed: _showSchedulePicker, 1439 1453 isActive: state.hasScheduledTime, 1440 1454 ); ··· 1457 1471 ), 1458 1472 ); 1459 1473 } 1474 + 1475 + String _localizedComposeError(BuildContext context, String message) { 1476 + final imageTooLarge = RegExp(r'^Image "(.+)" is ([0-9.]+) MB .+ max 1 MB\.$').firstMatch(message); 1477 + if (imageTooLarge != null) { 1478 + return context.l10n.formatComposeImageTooLarge(imageTooLarge.group(1)!, imageTooLarge.group(2)!); 1479 + } 1480 + 1481 + final videoTooLarge = RegExp(r'^Video is ([0-9.]+) MB .+ exceeds the 100 MB limit\.$').firstMatch(message); 1482 + if (videoTooLarge != null) { 1483 + return context.l10n.formatComposeVideoTooLarge(videoTooLarge.group(1)!); 1484 + } 1485 + 1486 + if (message.startsWith('Failed to pick image: ')) { 1487 + return context.l10n.formatComposeFailedToPickImage(message.substring('Failed to pick image: '.length)); 1488 + } 1489 + if (message.startsWith('Failed to pick video: ')) { 1490 + return context.l10n.formatComposeFailedToPickVideo(message.substring('Failed to pick video: '.length)); 1491 + } 1492 + if (message.startsWith('Failed to save changes: ')) { 1493 + return context.l10n.formatComposeFailedToSaveChanges(message.substring('Failed to save changes: '.length)); 1494 + } 1495 + if (message.startsWith('Failed to submit post: ')) { 1496 + return context.l10n.formatComposeFailedToSubmitPost(message.substring('Failed to submit post: '.length)); 1497 + } 1498 + if (message.startsWith('Upload failed: ')) { 1499 + return context.l10n.messageVideoUploadFailed; 1500 + } 1501 + 1502 + return switch (message) { 1503 + 'Daily video upload limit reached.' => context.l10n.messageVideoDailyUploadLimitReached, 1504 + 'Upload failed — please try again.' => context.l10n.messageVideoUploadFailed, 1505 + 'Video processing failed.' => context.l10n.messageVideoProcessingFailed, 1506 + 'Video processing timed out.' => context.l10n.messageVideoProcessingTimedOut, 1507 + 'Edit context is missing. Please reopen the editor and try again.' => context.l10n.errorComposeEditContextMissing, 1508 + 'Failed to save changes. Please try again.' => context.l10n.errorComposeFailedToSaveChanges, 1509 + 'Image file not found. Please re-attach and try again.' => context.l10n.errorComposeImageFileNotFound, 1510 + 'Unsupported image format. Use JPEG, PNG, or WebP.' => context.l10n.errorComposeUnsupportedImageFormat, 1511 + 'Failed to upload image. Please try again.' => context.l10n.errorComposeFailedToUploadImage, 1512 + 'Failed to create post. Please try again.' => context.l10n.errorComposeFailedToCreatePost, 1513 + 'Network error — post saved as draft.' => context.l10n.errorComposeNetworkSavedAsDraft, 1514 + 'This post was changed elsewhere. Reopen it and try editing again.' => context.l10n.errorComposeChangedElsewhere, 1515 + 'Could not save changes. Your original post was restored.' => context.l10n.errorComposeOriginalPostRestored, 1516 + 'Could not save changes and we could not confirm recovery. Reopen the thread and verify the post.' => 1517 + context.l10n.errorComposeCouldNotSaveAndConfirmRecovery, 1518 + 'Edit was submitted but could not be confirmed yet. Please reopen the post and verify.' => 1519 + context.l10n.errorComposeCouldNotConfirmEdit, 1520 + _ => message, 1521 + }; 1522 + } 1460 1523 } 1461 1524 1462 1525 class _DraftListItem extends StatelessWidget { ··· 1484 1547 crossAxisAlignment: CrossAxisAlignment.start, 1485 1548 children: [ 1486 1549 Text( 1487 - draft.content.isEmpty ? '(No text)' : draft.content, 1550 + draft.content.isEmpty ? context.l10n.messageComposeNoText : draft.content, 1488 1551 maxLines: 2, 1489 1552 overflow: TextOverflow.ellipsis, 1490 1553 style: theme.textTheme.bodyMedium?.copyWith(height: 1.35), ··· 1502 1565 padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), 1503 1566 decoration: BoxDecoration(color: colorScheme.primary, borderRadius: BorderRadius.circular(4)), 1504 1567 child: Text( 1505 - 'Scheduled', 1568 + context.l10n.labelScheduled, 1506 1569 style: theme.textTheme.labelSmall?.copyWith( 1507 1570 color: colorScheme.onPrimary, 1508 1571 fontWeight: FontWeight.w700, ··· 1518 1581 IconButton( 1519 1582 icon: Icon(Icons.delete_outline, color: colorScheme.error), 1520 1583 onPressed: onDelete, 1521 - tooltip: 'Delete draft', 1584 + tooltip: context.l10n.labelDeleteDraft, 1522 1585 visualDensity: VisualDensity.compact, 1523 1586 ), 1524 1587 ], ··· 1636 1699 children: [ 1637 1700 Row( 1638 1701 children: [ 1639 - Expanded(child: Text('Alt text', style: theme.textTheme.titleLarge)), 1640 - IconButton(tooltip: 'Close', onPressed: widget.onCancel, icon: const Icon(Icons.close)), 1702 + Expanded( 1703 + child: Text(context.l10n.messageComposeImageAltTextTitle, style: theme.textTheme.titleLarge), 1704 + ), 1705 + IconButton( 1706 + tooltip: context.l10n.labelClose, 1707 + onPressed: widget.onCancel, 1708 + icon: const Icon(Icons.close), 1709 + ), 1641 1710 ], 1642 1711 ), 1643 1712 const SizedBox(height: 12), ··· 1665 1734 maxLines: 5, 1666 1735 maxLength: 1000, 1667 1736 textInputAction: TextInputAction.newline, 1668 - decoration: const InputDecoration(hintText: 'Describe the image', border: OutlineInputBorder()), 1737 + decoration: InputDecoration( 1738 + hintText: context.l10n.messageComposeDescribeImage, 1739 + border: const OutlineInputBorder(), 1740 + ), 1669 1741 ), 1670 1742 const SizedBox(height: 8), 1671 1743 Row( 1672 1744 mainAxisAlignment: MainAxisAlignment.end, 1673 1745 children: [ 1674 - TextButton(onPressed: widget.onCancel, child: const Text('Cancel')), 1746 + TextButton(onPressed: widget.onCancel, child: Text(context.l10n.buttonCancel)), 1675 1747 const SizedBox(width: 8), 1676 - FilledButton(onPressed: () => widget.onSave(_controller.text), child: const Text('Save')), 1748 + FilledButton(onPressed: () => widget.onSave(_controller.text), child: Text(context.l10n.buttonSave)), 1677 1749 ], 1678 1750 ), 1679 1751 ], ··· 1723 1795 children: [ 1724 1796 Row( 1725 1797 children: [ 1726 - Expanded(child: Text('Video alt text', style: theme.textTheme.titleLarge)), 1727 - IconButton(tooltip: 'Close', onPressed: widget.onCancel, icon: const Icon(Icons.close)), 1798 + Expanded( 1799 + child: Text(context.l10n.messageComposeVideoAltTextTitle, style: theme.textTheme.titleLarge), 1800 + ), 1801 + IconButton( 1802 + tooltip: context.l10n.labelClose, 1803 + onPressed: widget.onCancel, 1804 + icon: const Icon(Icons.close), 1805 + ), 1728 1806 ], 1729 1807 ), 1730 1808 const SizedBox(height: 12), ··· 1737 1815 maxLines: 5, 1738 1816 maxLength: 1000, 1739 1817 textInputAction: TextInputAction.newline, 1740 - decoration: const InputDecoration(hintText: 'Describe the video', border: OutlineInputBorder()), 1818 + decoration: InputDecoration( 1819 + hintText: context.l10n.messageComposeDescribeVideo, 1820 + border: const OutlineInputBorder(), 1821 + ), 1741 1822 ), 1742 1823 const SizedBox(height: 8), 1743 1824 Row( 1744 1825 mainAxisAlignment: MainAxisAlignment.end, 1745 1826 children: [ 1746 - TextButton(onPressed: widget.onCancel, child: const Text('Cancel')), 1827 + TextButton(onPressed: widget.onCancel, child: Text(context.l10n.buttonCancel)), 1747 1828 const SizedBox(width: 8), 1748 - FilledButton(onPressed: () => widget.onSave(_controller.text), child: const Text('Save')), 1829 + FilledButton(onPressed: () => widget.onSave(_controller.text), child: Text(context.l10n.buttonSave)), 1749 1830 ], 1750 1831 ), 1751 1832 ], ··· 1911 1992 Icon(Icons.videocam_outlined, size: 40, color: colorScheme.onSurfaceVariant), 1912 1993 const SizedBox(height: 10), 1913 1994 Text( 1914 - filename.isEmpty ? 'Video' : filename, 1995 + filename.isEmpty ? context.l10n.labelVideo : filename, 1915 1996 key: const ValueKey('video-alt-preview-filename'), 1916 1997 style: theme.textTheme.bodyMedium, 1917 1998 maxLines: 1, ··· 1921 2002 if (error != null) ...[ 1922 2003 const SizedBox(height: 4), 1923 2004 Text( 1924 - 'Preview unavailable', 2005 + context.l10n.messageComposePreviewUnavailable, 1925 2006 style: theme.textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), 1926 2007 textAlign: TextAlign.center, 1927 2008 ),
+37 -25
lib/features/devtools/presentation/dev_tools_screen.dart
··· 6 6 import 'package:flutter/material.dart'; 7 7 import 'package:flutter/services.dart'; 8 8 import 'package:flutter_bloc/flutter_bloc.dart'; 9 + import 'package:lazurite/core/l10n/l10n.dart'; 9 10 import 'package:lazurite/core/theme/typography.dart'; 10 11 import 'package:lazurite/core/theme/theme_extensions.dart'; 11 12 import 'package:lazurite/core/widgets/app_breadcrumbs.dart'; ··· 19 20 20 21 @override 21 22 Widget build(BuildContext context) { 23 + final l10n = context.l10n; 24 + 22 25 return Scaffold( 23 26 appBar: AppBar( 24 - title: const Text('PDS Explorer'), 27 + title: Text(l10n.labelPdsExplorer), 25 28 actions: [ 26 29 IconButton( 27 30 icon: const Icon(Icons.open_in_new), 28 - tooltip: 'Go to pds.ls', 31 + tooltip: l10n.tooltipGoToPdsLs, 29 32 onPressed: () => _openExternalUrl('https://pds.ls'), 30 33 ), 31 34 ], ··· 100 103 101 104 @override 102 105 Widget build(BuildContext context) { 106 + final l10n = context.l10n; 103 107 final shouldShowTypeahead = 104 108 _controller.text.trim().startsWith('@') && 105 109 (widget.state.isTypeaheadLoading || widget.state.typeaheadActors.isNotEmpty); ··· 114 118 Expanded( 115 119 child: TextField( 116 120 controller: _controller, 117 - decoration: const InputDecoration( 118 - hintText: 'Handle, DID, or at:// URI', 119 - border: OutlineInputBorder(), 120 - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12), 121 + decoration: InputDecoration( 122 + hintText: l10n.placeholderHandleDidOrAtUri, 123 + border: const OutlineInputBorder(), 124 + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), 121 125 isDense: true, 122 126 ), 123 127 style: AppTypography.googleSansCode(fontSize: 13), ··· 128 132 const SizedBox(width: 8), 129 133 FilledButton( 130 134 onPressed: widget.state.isLoading ? null : () => _resolve(_controller.text), 131 - child: const Text('Resolve'), 135 + child: Text(l10n.buttonResolve), 132 136 ), 133 137 ], 134 138 ), ··· 243 247 244 248 List<AppBreadcrumbItem> _items(BuildContext context) { 245 249 final cubit = context.read<DevToolsCubit>(); 246 - final repoLabel = state.repoHandle ?? state.handle ?? state.did ?? 'Repository'; 250 + final l10n = context.l10n; 251 + final repoLabel = state.repoHandle ?? state.handle ?? state.did ?? l10n.labelRepository; 247 252 final items = <AppBreadcrumbItem>[ 248 253 AppBreadcrumbItem( 249 254 label: repoLabel, ··· 266 271 if (state.selectedRecord != null) { 267 272 items.add( 268 273 AppBreadcrumbItem( 269 - label: state.selectedRecord!.rkey.isEmpty ? 'Record JSON' : state.selectedRecord!.rkey, 274 + label: state.selectedRecord!.rkey.isEmpty ? l10n.labelRecordJson : state.selectedRecord!.rkey, 270 275 tooltip: state.selectedRecord!.uri, 271 276 key: const ValueKey('dev-tools-breadcrumb-record'), 272 277 ), ··· 297 302 children: [ 298 303 Icon(Icons.error_outline, size: 48, color: context.colorScheme.error), 299 304 const SizedBox(height: 16), 300 - Text('Error', style: context.textTheme.titleMedium), 305 + Text(context.l10n.labelLogLevelError, style: context.textTheme.titleMedium), 301 306 const SizedBox(height: 8), 302 307 Text( 303 - state.errorMessage ?? 'Unknown error', 308 + state.errorMessage ?? context.l10n.errorUnknown, 304 309 textAlign: TextAlign.center, 305 310 style: context.textTheme.bodyMedium, 306 311 ), ··· 343 348 children: [ 344 349 Icon(Icons.explore_outlined, size: 64, color: context.colorScheme.outline), 345 350 const SizedBox(height: 16), 346 - Text('PDS Explorer', style: context.textTheme.titleMedium), 351 + Text(context.l10n.labelPdsExplorer, style: context.textTheme.titleMedium), 347 352 const SizedBox(height: 8), 348 353 Text( 349 - 'Enter a handle, DID, or AT-URI to explore\n' 350 - 'a user\'s repository.', 354 + context.l10n.messageDevtoolsEmptyState, 351 355 textAlign: TextAlign.center, 352 356 style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.outline), 353 357 ), ··· 355 359 TextButton.icon( 356 360 onPressed: () => _openExternalUrl('https://pds.ls'), 357 361 icon: const Icon(Icons.open_in_new, size: 16), 358 - label: const Text('Inspired by pds.ls'), 362 + label: Text(context.l10n.buttonInspiredByPdsLs), 359 363 ), 360 364 ], 361 365 ), ··· 395 399 child: Column( 396 400 crossAxisAlignment: CrossAxisAlignment.start, 397 401 children: [ 398 - Text(state.repoHandle ?? state.handle ?? 'Unknown', style: context.textTheme.titleMedium), 402 + Text( 403 + state.repoHandle ?? state.handle ?? context.l10n.labelUnknown, 404 + style: context.textTheme.titleMedium, 405 + ), 399 406 const SizedBox(height: 2), 400 407 Text( 401 408 state.did ?? '', ··· 413 420 runSpacing: 4, 414 421 children: [ 415 422 Text( 416 - '${state.collections.length} collections', 423 + context.l10n.formatCollectionsCount(state.collections.length), 417 424 style: theme.textTheme.bodySmall!.copyWith(color: theme.colorScheme.onSurface), 418 425 ), 419 426 Text( 420 427 totalRepoRecords == null 421 - ? (state.isCollectionCountsLoading ? 'Counting records...' : 'Record counts unavailable') 422 - : '$totalRepoRecords records', 428 + ? (state.isCollectionCountsLoading 429 + ? context.l10n.messageRecordCountsLoading 430 + : context.l10n.messageRecordCountsUnavailable) 431 + : context.l10n.formatRecordsCount(totalRepoRecords), 423 432 style: context.textTheme.bodySmall!.copyWith(color: context.colorScheme.onSurfaceVariant), 424 433 ), 425 434 ], ··· 430 439 Padding( 431 440 padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), 432 441 child: Text( 433 - 'COLLECTIONS', 442 + context.l10n.labelCollections, 434 443 style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600, letterSpacing: 0.5), 435 444 ), 436 445 ), ··· 556 565 ), 557 566 Text( 558 567 selectedCollection?.recordCount == null 559 - ? '${records.length} loaded' 560 - : '${records.length} of ${selectedCollection!.recordCount}', 568 + ? context.l10n.formatLoadedRecordsCount(records.length) 569 + : context.l10n.formatLoadedRecordsOfTotal(records.length, selectedCollection!.recordCount!), 561 570 style: context.textTheme.bodySmall, 562 571 ), 563 572 ], ··· 655 664 if (record.cid != null) ...[ 656 665 const SizedBox(height: 2), 657 666 Text( 658 - 'CID: ${record.cid!}', 667 + context.l10n.formatCid(record.cid!), 659 668 style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.secondary), 660 669 ), 661 670 ], ··· 666 675 children: [ 667 676 TextButton.icon( 668 677 icon: const Icon(Icons.copy, size: 16), 669 - label: const Text('Copy JSON'), 678 + label: Text(context.l10n.buttonCopyJson), 670 679 onPressed: () { 671 680 Clipboard.setData(ClipboardData(text: jsonString)); 672 681 ScaffoldMessenger.of(context).showSnackBar( 673 - const SnackBar(content: Text('JSON copied to clipboard'), behavior: SnackBarBehavior.floating), 682 + SnackBar( 683 + content: Text(context.l10n.messageJsonCopiedToClipboard), 684 + behavior: SnackBarBehavior.floating, 685 + ), 674 686 ); 675 687 }, 676 688 ),
+2 -1
lib/features/feed/presentation/home_feed_screen.dart
··· 5 5 import 'package:flutter_animate/flutter_animate.dart'; 6 6 import 'package:flutter_bloc/flutter_bloc.dart'; 7 7 import 'package:go_router/go_router.dart'; 8 + import 'package:lazurite/core/l10n/l10n.dart'; 8 9 import 'package:lazurite/core/theme/animation_tokens.dart'; 9 10 import 'package:lazurite/core/theme/animation_utils.dart'; 10 11 import 'package:lazurite/core/widgets/lazurite_app_bar.dart'; ··· 105 106 actions: [ 106 107 IconButton( 107 108 icon: const Icon(Icons.trending_up_outlined), 108 - tooltip: 'Trending', 109 + tooltip: context.l10n.labelTrending, 109 110 onPressed: () => context.push('/trending'), 110 111 ), 111 112 IconButton(
+4 -3
lib/features/feed/presentation/post_thread_screen.dart
··· 8 8 import 'package:flutter_bloc/flutter_bloc.dart'; 9 9 import 'package:go_router/go_router.dart'; 10 10 import 'package:intl/intl.dart'; 11 + import 'package:lazurite/core/l10n/l10n.dart'; 11 12 import 'package:lazurite/core/theme/theme_extensions.dart'; 12 13 import 'package:lazurite/features/compose/presentation/compose_route_args.dart'; 13 14 import 'package:lazurite/features/connectivity/cubit/connectivity_cubit.dart'; ··· 1036 1037 Future<void> _confirmDelete(BuildContext context) async { 1037 1038 await showConfirmationDialog( 1038 1039 context: context, 1039 - title: const Text('Delete Post?'), 1040 - content: const Text('This action cannot be undone.'), 1041 - confirmLabel: 'Delete', 1040 + title: Text(context.l10n.dialogDeletePostTitle), 1041 + content: Text(context.l10n.dialogDeletePostContent), 1042 + confirmLabel: context.l10n.buttonDelete, 1042 1043 confirmDestructive: true, 1043 1044 onConfirmed: () => context.read<PostActionCubit>().deletePost(), 1044 1045 );
+57 -55
lib/features/feed/presentation/saved_posts_screen.dart
··· 6 6 import 'package:flutter_bloc/flutter_bloc.dart'; 7 7 import 'package:go_router/go_router.dart'; 8 8 import 'package:lazurite/core/database/app_database.dart'; 9 + import 'package:lazurite/core/l10n/l10n.dart'; 9 10 import 'package:lazurite/core/logging/app_logger.dart'; 10 11 import 'package:lazurite/core/network/app_view_provider.dart'; 11 12 import 'package:lazurite/core/network/app_view_web_links.dart'; ··· 22 23 import 'package:lazurite/shared/presentation/widgets/error_state.dart'; 23 24 import 'package:lazurite/shared/presentation/widgets/loading_state.dart'; 24 25 import 'package:lazurite/shared/presentation/widgets/staggered_entrance.dart'; 26 + import 'package:lazurite/shared/utils/format_utils.dart'; 25 27 26 28 enum SavedPostsInitialTab { bookmarks, liked, search } 27 29 ··· 72 74 Widget build(BuildContext context) { 73 75 return Scaffold( 74 76 appBar: AppBar( 75 - title: const Text('Bookmarks & Likes'), 77 + title: Text(context.l10n.labelBookmarksAndLikes), 76 78 bottom: TabBar( 77 79 controller: _tabController, 78 - tabs: const [ 79 - Tab(text: 'Bookmarks'), 80 - Tab(text: 'Liked'), 81 - Tab(text: 'Search'), 80 + tabs: [ 81 + Tab(text: context.l10n.labelBookmarks), 82 + Tab(text: context.l10n.labelLiked), 83 + Tab(text: context.l10n.labelSearch), 82 84 ], 83 85 ), 84 86 ), ··· 111 113 112 114 if (state.status == SavedPostsStatus.error) { 113 115 return ErrorState( 114 - title: 'Failed to load bookmarks', 115 - message: state.error ?? 'Unknown error', 116 + title: context.l10n.errorFailedToLoadBookmarks, 117 + message: state.error ?? context.l10n.errorUnknown, 116 118 onRetry: () => context.read<SavedPostsCubit>().loadSavedPosts(), 117 119 ); 118 120 } ··· 125 127 .toList(growable: false); 126 128 127 129 if (localPosts.isEmpty && cloudPosts.isEmpty) { 128 - return const EmptyState( 129 - message: 'No bookmarks', 130 - subtitle: 'Posts you bookmark will appear here', 130 + return EmptyState( 131 + message: context.l10n.messageNoBookmarks, 132 + subtitle: context.l10n.messageNoBookmarksSubtitle, 131 133 icon: Icons.bookmark_outline, 132 134 ); 133 135 } ··· 140 142 padding: const EdgeInsets.fromLTRB(12, 8, 8, 0), 141 143 child: Row( 142 144 children: [ 143 - const Expanded( 145 + Expanded( 144 146 child: TabBar( 145 147 isScrollable: true, 146 148 tabAlignment: TabAlignment.start, 147 149 tabs: [ 148 - Tab(text: 'Local'), 149 - Tab(text: 'Bluesky'), 150 + Tab(text: context.l10n.labelLocal), 151 + Tab(text: context.l10n.labelBluesky), 150 152 ], 151 153 ), 152 154 ), 153 155 if (localPosts.isNotEmpty) 154 156 PopupMenuButton<_BookmarksMenuAction>( 155 - tooltip: 'Bookmark actions', 157 + tooltip: context.l10n.labelBookmarkActions, 156 158 onSelected: (action) { 157 159 if (action == _BookmarksMenuAction.clearLocal) { 158 160 _confirmClearLocal(context); 159 161 } 160 162 }, 161 - itemBuilder: (context) => const [ 163 + itemBuilder: (context) => [ 162 164 PopupMenuItem( 163 165 value: _BookmarksMenuAction.clearLocal, 164 166 child: ListTile( 165 - leading: Icon(Icons.delete_sweep_outlined), 166 - title: Text('Clear local bookmarks'), 167 + leading: const Icon(Icons.delete_sweep_outlined), 168 + title: Text(context.l10n.labelClearLocalBookmarks), 167 169 contentPadding: EdgeInsets.zero, 168 170 dense: true, 169 171 ), ··· 205 207 return AnimatedRefreshIndicator( 206 208 onRefresh: onRefresh, 207 209 child: ListView( 208 - children: const [ 209 - SizedBox(height: 80), 210 + children: [ 211 + const SizedBox(height: 80), 210 212 EmptyState( 211 - message: 'No bookmarks in this source', 212 - subtitle: 'Try switching tabs or saving posts to this source', 213 + message: context.l10n.messageNoBookmarksInSource, 214 + subtitle: context.l10n.messageNoBookmarksInSourceSubtitle, 213 215 icon: Icons.bookmark_border, 214 216 ), 215 217 ], ··· 241 243 showDialog<void>( 242 244 context: context, 243 245 builder: (dialogContext) => AlertDialog( 244 - title: const Text('Clear local bookmarks?'), 245 - content: const Text( 246 - 'This removes only local bookmarks from this device. Bluesky cloud bookmarks will not be deleted.', 247 - ), 246 + title: Text(context.l10n.dialogClearLocalBookmarksTitle), 247 + content: Text(context.l10n.dialogClearLocalBookmarksContent), 248 248 actions: [ 249 - TextButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('Cancel')), 249 + TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(context.l10n.buttonCancel)), 250 250 FilledButton( 251 251 onPressed: () { 252 252 Navigator.pop(dialogContext); ··· 256 256 backgroundColor: context.colorScheme.error, 257 257 foregroundColor: context.colorScheme.onError, 258 258 ), 259 - child: const Text('Clear Local'), 259 + child: Text(context.l10n.buttonClearLocal), 260 260 ), 261 261 ], 262 262 ), ··· 294 294 } catch (_) { 295 295 setState(() { 296 296 _isLoading = false; 297 - _error = 'Liked posts are unavailable right now.'; 297 + _error = context.l10n.messageLikedPostsUnavailable; 298 298 }); 299 299 return; 300 300 } ··· 325 325 } 326 326 setState(() { 327 327 _isLoading = false; 328 - _error = 'Failed to load liked posts: $e'; 328 + _error = context.l10n.errorFailedToLoadLikedPostsDetails(e); 329 329 }); 330 330 } 331 331 } ··· 368 368 setState(() { 369 369 _isLoading = false; 370 370 _isSyncing = false; 371 - _syncWarning = 'Failed to refresh liked posts: $e'; 371 + _syncWarning = context.l10n.errorFailedToRefreshLikedPosts(e); 372 372 }); 373 373 return; 374 374 } 375 375 setState(() { 376 376 _isLoading = false; 377 377 _isSyncing = false; 378 - _error = 'Failed to load liked posts: $e'; 378 + _error = context.l10n.errorFailedToLoadLikedPostsDetails(e); 379 379 }); 380 380 } 381 381 } ··· 398 398 } 399 399 400 400 if (_error != null && _likedPosts.isEmpty) { 401 - return ErrorState(title: 'Failed to load liked posts', message: _error!, onRetry: () => _syncAndReload()); 401 + return ErrorState( 402 + title: context.l10n.errorFailedToLoadLikedPosts, 403 + message: _error!, 404 + onRetry: () => _syncAndReload(), 405 + ); 402 406 } 403 407 404 408 if (_likedPosts.isEmpty) { 405 409 return AnimatedRefreshIndicator( 406 410 onRefresh: _syncAndReload, 407 411 child: ListView( 408 - children: const [ 409 - SizedBox(height: 80), 412 + children: [ 413 + const SizedBox(height: 80), 410 414 EmptyState( 411 - message: 'No liked posts', 412 - subtitle: 'Posts you like will appear here after sync', 415 + message: context.l10n.messageNoLikedPosts, 416 + subtitle: context.l10n.messageNoLikedPostsSubtitle, 413 417 icon: Icons.favorite_outline, 414 418 ), 415 419 ], ··· 510 514 margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 511 515 child: ListTile( 512 516 leading: const Icon(Icons.bookmark), 513 - title: const Text('Bookmarked Post'), 514 - subtitle: Text('Saved on ${_formatDate(savedPost.savedAt)}', style: context.textTheme.bodySmall), 517 + title: Text(context.l10n.labelBookmarkedPost), 518 + subtitle: Text( 519 + context.l10n.formatSavedOn(_formatDate(context, savedPost.savedAt)), 520 + style: context.textTheme.bodySmall, 521 + ), 515 522 trailing: Row( 516 523 mainAxisSize: MainAxisSize.min, 517 524 children: [ 518 525 IconButton( 519 526 icon: const Icon(Icons.open_in_new), 520 527 onPressed: () => context.push('/post?uri=${Uri.encodeQueryComponent(savedPost.postUri)}'), 521 - tooltip: 'Open post', 528 + tooltip: context.l10n.labelOpenPost, 522 529 ), 523 530 IconButton( 524 531 icon: const Icon(Icons.share_outlined), ··· 526 533 context, 527 534 AppViewWebLinks.postFromAtUri(savedPost.postUri, appViewProvider: _resolveAppViewProvider(context)), 528 535 ), 529 - tooltip: 'Share', 536 + tooltip: context.l10n.buttonShare, 530 537 ), 531 - IconButton(icon: const Icon(Icons.delete_outline), onPressed: onUnsave, tooltip: 'Remove'), 538 + IconButton(icon: const Icon(Icons.delete_outline), onPressed: onUnsave, tooltip: context.l10n.buttonRemove), 532 539 ], 533 540 ), 534 541 ), ··· 589 596 margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 590 597 child: ListTile( 591 598 leading: const Icon(Icons.favorite_outline), 592 - title: const Text('Liked Post'), 593 - subtitle: Text('Liked on ${_formatDate(likedPost.likedAt)}', style: context.textTheme.bodySmall), 599 + title: Text(context.l10n.labelLikedPost), 600 + subtitle: Text( 601 + context.l10n.formatLikedOn(_formatDate(context, likedPost.likedAt)), 602 + style: context.textTheme.bodySmall, 603 + ), 594 604 trailing: Row( 595 605 mainAxisSize: MainAxisSize.min, 596 606 children: [ 597 607 IconButton( 598 608 icon: const Icon(Icons.open_in_new), 599 609 onPressed: () => context.push('/post?uri=${Uri.encodeQueryComponent(likedPost.postUri)}'), 600 - tooltip: 'Open post', 610 + tooltip: context.l10n.labelOpenPost, 601 611 ), 602 - IconButton(icon: const Icon(Icons.delete_outline), onPressed: onRemove, tooltip: 'Remove'), 612 + IconButton(icon: const Icon(Icons.delete_outline), onPressed: onRemove, tooltip: context.l10n.buttonRemove), 603 613 ], 604 614 ), 605 615 ), ··· 607 617 } 608 618 } 609 619 610 - String _formatDate(DateTime date) { 611 - final now = DateTime.now(); 612 - final difference = now.difference(date); 613 - 614 - if (difference.inMinutes < 1) return 'just now'; 615 - if (difference.inHours < 1) return '${difference.inMinutes}m ago'; 616 - if (difference.inDays < 1) return '${difference.inHours}h ago'; 617 - if (difference.inDays < 7) return '${difference.inDays}d ago'; 618 - return '${date.month}/${date.day}/${date.year}'; 619 - } 620 + String _formatDate(BuildContext context, DateTime date) => 621 + formatRelativeTime(date, includeAgo: true, locale: Localizations.localeOf(context).toString());
+19 -13
lib/features/feed/presentation/trending_screen.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:flutter_bloc/flutter_bloc.dart'; 3 3 import 'package:go_router/go_router.dart'; 4 + import 'package:lazurite/core/l10n/l10n.dart'; 4 5 import 'package:lazurite/core/network/app_view_provider.dart'; 5 6 import 'package:lazurite/core/network/app_view_router.dart'; 6 7 import 'package:lazurite/core/widgets/lazurite_app_bar.dart'; ··· 22 23 class _TrendingScreenState extends State<TrendingScreen> { 23 24 TrendingScreenData? _data; 24 25 bool _loading = true; 25 - String? _errorMessage; 26 + Object? _error; 26 27 27 28 @override 28 29 void initState() { ··· 37 38 38 39 setState(() { 39 40 _loading = true; 40 - _errorMessage = null; 41 + _error = null; 41 42 }); 42 43 43 44 try { ··· 56 57 } 57 58 58 59 setState(() { 59 - _errorMessage = 'Failed to load trending topics: $error'; 60 + _error = error; 60 61 _loading = false; 61 62 }); 62 63 } ··· 79 80 @override 80 81 Widget build(BuildContext context) { 81 82 return Scaffold( 82 - appBar: const LazuriteAppBar(sectionLabel: 'Trending'), 83 + appBar: LazuriteAppBar(sectionLabel: context.l10n.labelTrending), 83 84 body: _buildBody(context), 84 85 ); 85 86 } 86 87 87 88 Widget _buildBody(BuildContext context) { 88 89 if (_loading) { 89 - return const LoadingState(message: 'Loading trending topics'); 90 + return LoadingState(message: context.l10n.messageLoadingTrendingTopics); 90 91 } 91 92 92 - if (_errorMessage != null) { 93 - return ErrorState(title: 'Failed to load trending', message: _errorMessage!, onRetry: _load); 93 + final error = _error; 94 + if (error != null) { 95 + return ErrorState( 96 + title: context.l10n.errorFailedToLoadTrending, 97 + message: context.l10n.errorFailedToLoadTrendingTopics(error), 98 + onRetry: _load, 99 + ); 94 100 } 95 101 96 102 final data = _data; 97 103 if (data == null || data.isEmpty) { 98 - return const EmptyState(icon: Icons.trending_up_outlined, message: 'No trending topics right now'); 104 + return EmptyState(icon: Icons.trending_up_outlined, message: context.l10n.messageNoTrendingTopicsRightNow); 99 105 } 100 106 101 107 final rows = <Widget>[ ··· 109 115 borderRadius: BorderRadius.circular(12), 110 116 ), 111 117 padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), 112 - child: const Text('Metadata temporarily unavailable'), 118 + child: Text(context.l10n.messageMetadataTemporarilyUnavailable), 113 119 ), 114 120 ), 115 - const _SectionHeader(title: 'Topics'), 121 + _SectionHeader(title: context.l10n.labelTopics), 116 122 ...data.topics.map((item) => _TrendTile(item: item, onTap: () => _onTapTopic(item))), 117 - if (data.suggested.isNotEmpty) const _SectionHeader(title: 'Suggested'), 123 + if (data.suggested.isNotEmpty) _SectionHeader(title: context.l10n.labelSuggested), 118 124 ...data.suggested.map((item) => _TrendTile(item: item, onTap: () => _onTapTopic(item))), 119 125 const SizedBox(height: 16), 120 126 ]; ··· 155 161 subtitleLines.add(description); 156 162 } 157 163 if (trend != null) { 158 - subtitleLines.add('${trend.postCount} posts'); 164 + subtitleLines.add(context.l10n.formatTrendingPostCount(trend.postCount)); 159 165 if (trend.category != null && trend.category!.trim().isNotEmpty) { 160 - subtitleLines.add('Category: ${trend.category}'); 166 + subtitleLines.add(context.l10n.formatTrendingCategory(trend.category!)); 161 167 } 162 168 } 163 169
+2 -1
lib/features/feed/presentation/widgets/compact_post_card.dart
··· 2 2 import 'package:bluesky/app_bsky_feed_defs.dart'; 3 3 import 'package:bluesky/moderation.dart' as bsky_moderation; 4 4 import 'package:flutter/material.dart'; 5 + import 'package:lazurite/core/l10n/l10n.dart'; 5 6 import 'package:lazurite/core/theme/theme_extensions.dart'; 6 7 import 'package:lazurite/features/feed/presentation/widgets/facet_text.dart'; 7 8 import 'package:lazurite/features/feed/presentation/widgets/post_embed_view.dart'; ··· 104 105 ), 105 106 const SizedBox(height: 2), 106 107 Text( 107 - '@${author.handle} · ${formatRelativeTime(createdAt)}', 108 + '@${author.handle} · ${formatRelativeTime(createdAt, nowLabel: context.l10n.commonNow)}', 108 109 style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 109 110 ), 110 111 ],
+6 -3
lib/features/feed/presentation/widgets/grid_post_card.dart
··· 5 5 import 'package:cached_network_image/cached_network_image.dart'; 6 6 import 'package:flutter/material.dart'; 7 7 import 'package:lazurite/core/cache/lazurite_image_cache.dart'; 8 + import 'package:lazurite/core/l10n/l10n.dart'; 8 9 import 'package:lazurite/core/theme/color_filters.dart'; 9 10 import 'package:lazurite/core/theme/spacing.dart'; 10 11 import 'package:lazurite/core/theme/theme_extensions.dart'; ··· 63 64 ? PostEmbedView(feedViewPost: feedViewPost, embed: post.embed!, compact: isCompactGrid) 64 65 : null; 65 66 66 - final resolvedFooter = footer ?? PostCardFooter(timestamp: formatPostTime(record?.createdAt ?? post.indexedAt)); 67 + final resolvedFooter = 68 + footer ?? 69 + PostCardFooter(timestamp: formatPostTime(record?.createdAt ?? post.indexedAt, nowLabel: context.l10n.labelNow)); 67 70 68 71 return Container( 69 72 decoration: BoxDecoration( ··· 236 239 const SizedBox(width: 6), 237 240 Expanded( 238 241 child: Text( 239 - 'Reply in a thread', 242 + context.l10n.messageReplyInThread, 240 243 maxLines: 1, 241 244 overflow: TextOverflow.ellipsis, 242 245 style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), ··· 259 262 crossAxisAlignment: CrossAxisAlignment.start, 260 263 children: [ 261 264 Text( 262 - 'Replying to @${parentPost.author.handle}', 265 + context.l10n.formatReplyingToHandle(parentPost.author.handle), 263 266 maxLines: 1, 264 267 overflow: TextOverflow.ellipsis, 265 268 style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurfaceVariant),
+10 -10
lib/features/feed/presentation/widgets/post_action_bar.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:flutter_animate/flutter_animate.dart'; 3 3 import 'package:flutter_bloc/flutter_bloc.dart'; 4 + import 'package:lazurite/core/l10n/l10n.dart'; 4 5 import 'package:lazurite/core/network/app_view_provider.dart'; 5 6 import 'package:lazurite/core/network/app_view_web_links.dart'; 6 7 import 'package:lazurite/core/theme/animation_tokens.dart'; 7 - import 'package:lazurite/features/connectivity/connectivity_helpers.dart'; 8 8 import 'package:lazurite/features/settings/bloc/settings_cubit.dart'; 9 9 import 'package:lazurite/shared/presentation/helpers/haptic_helper.dart'; 10 10 import 'package:lazurite/shared/presentation/helpers/share_helper.dart'; ··· 74 74 activeIcon: Icons.chat_bubble, 75 75 count: replyCount, 76 76 onTap: isOffline ? null : onReply, 77 - tooltip: isOffline ? offlineActionMessage('reply to this post') : null, 77 + tooltip: isOffline ? context.l10n.formatOfflineReconnectAction(context.l10n.actionReplyToThisPost) : null, 78 78 color: context.colorScheme.onSurfaceVariant, 79 79 ), 80 80 _ActionButton( ··· 87 87 onTap: isOffline ? null : onRepost, 88 88 activeColor: Colors.green, 89 89 onLongPress: !isOffline && onRepost != null ? () => _showRepostOptions(context) : null, 90 - tooltip: isOffline ? offlineActionMessage('repost this post') : null, 90 + tooltip: isOffline ? context.l10n.formatOfflineReconnectAction(context.l10n.actionRepostThisPost) : null, 91 91 ), 92 92 _ActionButton( 93 93 icon: Icons.favorite_outline, ··· 98 98 animateOnTap: true, 99 99 onTap: isOffline ? null : onLike, 100 100 activeColor: Colors.pink, 101 - tooltip: isOffline ? offlineActionMessage('like this post') : null, 101 + tooltip: isOffline ? context.l10n.formatOfflineReconnectAction(context.l10n.actionLikeThisPost) : null, 102 102 ), 103 103 _ActionButton( 104 104 icon: isSaved ? Icons.bookmark : Icons.bookmark_outline, ··· 137 137 items: [ 138 138 OptionsSheetItem( 139 139 leading: Icon(Icons.repeat, color: isReposted ? Colors.green : null), 140 - title: isReposted ? 'Unrepost' : 'Repost', 141 - subtitle: isReposted ? 'Remove this repost' : 'Share this post', 140 + title: isReposted ? context.l10n.labelUnrepost : context.l10n.labelRepost, 141 + subtitle: isReposted ? context.l10n.messageRemoveRepostSubtitle : context.l10n.messageShareThisPostSubtitle, 142 142 onTap: onRepost, 143 143 ), 144 144 if (!isReposted) 145 145 OptionsSheetItem( 146 146 leading: const Icon(Icons.format_quote), 147 - title: 'Quote Post', 148 - subtitle: 'Quote this post with your own text', 147 + title: context.l10n.labelQuotePost, 148 + subtitle: context.l10n.messageQuotePostSubtitle, 149 149 onTap: onQuote, 150 150 ), 151 151 ], ··· 164 164 isLocalSaved ? Icons.bookmark_remove_outlined : Icons.bookmark_add_outlined, 165 165 color: Colors.amber, 166 166 ), 167 - title: isLocalSaved ? 'Remove local save' : 'Save locally', 167 + title: isLocalSaved ? context.l10n.labelRemoveLocalSave : context.l10n.labelSaveLocally, 168 168 onTap: onSave, 169 169 ), 170 170 OptionsSheetItem( ··· 172 172 isCloudSaved ? Icons.cloud_off_outlined : Icons.cloud_outlined, 173 173 color: context.colorScheme.primary, 174 174 ), 175 - title: isCloudSaved ? 'Remove from Bluesky' : 'Save to Bluesky', 175 + title: isCloudSaved ? context.l10n.labelRemoveFromBluesky : context.l10n.labelSaveToBluesky, 176 176 onTap: isCloudSaved ? onCloudUnsave : onCloudSave, 177 177 ), 178 178 ],
+6 -3
lib/features/feed/presentation/widgets/post_card.dart
··· 2 2 import 'package:bluesky/app_bsky_feed_defs.dart'; 3 3 import 'package:bluesky/moderation.dart' as bsky_moderation; 4 4 import 'package:flutter/material.dart'; 5 + import 'package:lazurite/core/l10n/l10n.dart'; 5 6 import 'package:lazurite/core/theme/theme_extensions.dart'; 6 7 import 'package:lazurite/features/feed/presentation/widgets/facet_text.dart'; 7 8 import 'package:lazurite/features/feed/presentation/widgets/post_card_footer.dart'; ··· 44 45 final moderationService = maybeModerationService(context); 45 46 final postUi = moderationService?.postUi(post, moderationContext) ?? const bsky_moderation.ModerationUI(); 46 47 47 - final resolvedFooter = actionBar ?? PostCardFooter(timestamp: formatPostTime(record?.createdAt ?? post.indexedAt)); 48 + final resolvedFooter = 49 + actionBar ?? 50 + PostCardFooter(timestamp: formatPostTime(record?.createdAt ?? post.indexedAt, nowLabel: context.l10n.labelNow)); 48 51 49 52 return Container( 50 53 margin: const EdgeInsets.symmetric(vertical: 1), ··· 134 137 const SizedBox(width: 6), 135 138 Flexible( 136 139 child: Text( 137 - 'Reply in a thread', 140 + context.l10n.messageReplyInThread, 138 141 maxLines: 1, 139 142 overflow: TextOverflow.ellipsis, 140 143 style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), ··· 157 160 crossAxisAlignment: CrossAxisAlignment.start, 158 161 children: [ 159 162 Text( 160 - 'Replying to @${parentPost.author.handle}', 163 + context.l10n.formatReplyingToHandle(parentPost.author.handle), 161 164 style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 162 165 ), 163 166 if (parentText.isNotEmpty) ...[
+12 -12
lib/features/feed/presentation/widgets/post_card_footer.dart
··· 1 1 import 'package:flutter/material.dart'; 2 - import 'package:lazurite/features/connectivity/connectivity_helpers.dart'; 2 + import 'package:lazurite/core/l10n/l10n.dart'; 3 3 import 'package:lazurite/shared/presentation/helpers/haptic_helper.dart'; 4 4 import 'package:lazurite/shared/presentation/widgets/options_sheet.dart'; 5 5 import 'package:lazurite/shared/utils/format_utils.dart'; 6 6 import 'package:lazurite/core/theme/theme_extensions.dart'; 7 7 8 8 /// Formats a post timestamp as a short, uppercase string. 9 - String formatPostTime(DateTime time) { 10 - return formatRelativeTime(time, nowLabel: 'NOW', uppercase: true); 9 + String formatPostTime(DateTime time, {String nowLabel = 'NOW'}) { 10 + return formatRelativeTime(time, nowLabel: nowLabel, uppercase: true); 11 11 } 12 12 13 13 /// Shared footer for post cards. Renders a top-bordered row with ··· 93 93 verticalPadding: actionVerticalPadding, 94 94 minTapTarget: minimumTapTarget, 95 95 showCount: canShowCounts, 96 - tooltip: isOffline ? offlineActionMessage('reply to this post') : null, 96 + tooltip: isOffline ? context.l10n.formatOfflineReconnectAction(context.l10n.actionReplyToThisPost) : null, 97 97 ), 98 98 _FooterAction( 99 99 icon: Icons.repeat, ··· 110 110 verticalPadding: actionVerticalPadding, 111 111 minTapTarget: minimumTapTarget, 112 112 showCount: canShowCounts, 113 - tooltip: isOffline ? offlineActionMessage('repost this post') : null, 113 + tooltip: isOffline ? context.l10n.formatOfflineReconnectAction(context.l10n.actionRepostThisPost) : null, 114 114 ), 115 115 _FooterAction( 116 116 icon: Icons.favorite_outline, ··· 126 126 verticalPadding: actionVerticalPadding, 127 127 minTapTarget: minimumTapTarget, 128 128 showCount: canShowCounts, 129 - tooltip: isOffline ? offlineActionMessage('like this post') : null, 129 + tooltip: isOffline ? context.l10n.formatOfflineReconnectAction(context.l10n.actionLikeThisPost) : null, 130 130 ), 131 131 _FooterAction( 132 132 icon: isSaved ? Icons.bookmark : Icons.bookmark_outline, ··· 194 194 items: [ 195 195 OptionsSheetItem( 196 196 leading: Icon(Icons.repeat, color: isReposted ? Colors.green : null), 197 - title: isReposted ? 'Unrepost' : 'Repost', 198 - subtitle: isReposted ? 'Remove this repost' : 'Share this post', 197 + title: isReposted ? context.l10n.labelUnrepost : context.l10n.labelRepost, 198 + subtitle: isReposted ? context.l10n.messageRemoveRepostSubtitle : context.l10n.messageShareThisPostSubtitle, 199 199 onTap: onRepost, 200 200 ), 201 201 if (!isReposted) 202 202 OptionsSheetItem( 203 203 leading: const Icon(Icons.format_quote), 204 - title: 'Quote Post', 205 - subtitle: 'Quote this post with your own text', 204 + title: context.l10n.labelQuotePost, 205 + subtitle: context.l10n.messageQuotePostSubtitle, 206 206 onTap: onQuote, 207 207 ), 208 208 ], ··· 270 270 isLocalSaved ? Icons.bookmark_remove_outlined : Icons.bookmark_add_outlined, 271 271 color: Colors.amber, 272 272 ), 273 - title: isLocalSaved ? 'Remove local save' : 'Save locally', 273 + title: isLocalSaved ? context.l10n.labelRemoveLocalSave : context.l10n.labelSaveLocally, 274 274 onTap: onSave, 275 275 ), 276 276 OptionsSheetItem( ··· 278 278 isCloudSaved ? Icons.cloud_off_outlined : Icons.cloud_outlined, 279 279 color: context.colorScheme.primary, 280 280 ), 281 - title: isCloudSaved ? 'Remove from Bluesky' : 'Save to Bluesky', 281 + title: isCloudSaved ? context.l10n.labelRemoveFromBluesky : context.l10n.labelSaveToBluesky, 282 282 onTap: isCloudSaved ? onCloudUnsave : onCloudSave, 283 283 ), 284 284 ],
+7 -6
lib/features/feed/presentation/widgets/post_card_with_actions.dart
··· 6 6 import 'package:bluesky/moderation.dart' as bsky_moderation; 7 7 import 'package:flutter/material.dart'; 8 8 import 'package:flutter_bloc/flutter_bloc.dart'; 9 + import 'package:lazurite/core/l10n/l10n.dart'; 9 10 import 'package:go_router/go_router.dart'; 10 11 import 'package:lazurite/core/theme/feed_layout.dart'; 11 12 import 'package:lazurite/features/connectivity/cubit/connectivity_cubit.dart'; ··· 145 146 (previous.error != current.error && current.error != null) || (!previous.isDeleted && current.isDeleted), 146 147 listener: (context, state) { 147 148 if (state.isDeleted) { 148 - showAppSnackBar(context, 'Post deleted', behavior: SnackBarBehavior.floating); 149 + showAppSnackBar(context, context.l10n.messagePostDeleted, behavior: SnackBarBehavior.floating); 149 150 onDeleted?.call(); 150 151 return; 151 152 } ··· 156 157 context, 157 158 error, 158 159 behavior: SnackBarBehavior.floating, 159 - actionLabel: 'Retry', 160 + actionLabel: context.l10n.buttonRetry, 160 161 onAction: () { 161 162 if (error.contains('like')) { 162 163 cubit.toggleLike(); ··· 222 223 return BlocBuilder<SavedPostsCubit, SavedPostsState>( 223 224 builder: (context, savedState) { 224 225 return PostCardFooter( 225 - timestamp: formatPostTime(post.indexedAt), 226 + timestamp: formatPostTime(post.indexedAt, nowLabel: context.l10n.labelNow), 226 227 replyCount: post.replyCount ?? 0, 227 228 repostCount: postActionState.repostCount, 228 229 likeCount: postActionState.likeCount, ··· 365 366 Future<void> _confirmDelete(BuildContext context) async { 366 367 await showConfirmationDialog( 367 368 context: context, 368 - title: const Text('Delete Post?'), 369 - content: const Text('This action cannot be undone.'), 370 - confirmLabel: 'Delete', 369 + title: Text(context.l10n.dialogDeletePostTitle), 370 + content: Text(context.l10n.dialogDeletePostContent), 371 + confirmLabel: context.l10n.buttonDelete, 371 372 confirmDestructive: true, 372 373 onConfirmed: () => context.read<PostActionCubit>().deletePost(), 373 374 );
+10 -6
lib/features/feed/presentation/widgets/post_embed_view.dart
··· 9 9 import 'package:flutter/material.dart'; 10 10 import 'package:go_router/go_router.dart'; 11 11 import 'package:lazurite/core/cache/lazurite_image_cache.dart'; 12 + import 'package:lazurite/core/l10n/l10n.dart'; 12 13 import 'package:lazurite/core/theme/theme_extensions.dart'; 13 14 import 'package:lazurite/features/feed/presentation/media/image_viewer_route_args.dart'; 14 15 import 'package:lazurite/features/feed/presentation/media/media_actions.dart'; ··· 271 272 } 272 273 273 274 if (record.isEmbedRecordViewNotFound) { 274 - return _buildUnavailableQuote(context, 'Quoted post not found'); 275 + return _buildUnavailableQuote(context, context.l10n.messageQuotedPostNotFound); 275 276 } 276 277 if (record.isEmbedRecordViewBlocked) { 277 - return _buildUnavailableQuote(context, 'Quoted post is blocked'); 278 + return _buildUnavailableQuote(context, context.l10n.messageQuotedPostBlocked); 278 279 } 279 280 if (record.isEmbedRecordViewDetached) { 280 - return _buildUnavailableQuote(context, 'Quoted post is unavailable'); 281 + return _buildUnavailableQuote(context, context.l10n.messageQuotedPostUnavailable); 281 282 } 282 283 283 284 return const SizedBox.shrink(); ··· 359 360 final selected = await showMenu<_ImageThumbnailAction>( 360 361 context: context, 361 362 position: RelativeRect.fromLTRB(globalPosition.dx, globalPosition.dy, globalPosition.dx, globalPosition.dy), 362 - items: const [ 363 - PopupMenuItem<_ImageThumbnailAction>(value: _ImageThumbnailAction.save, child: Text('Save image')), 364 - PopupMenuItem<_ImageThumbnailAction>(value: _ImageThumbnailAction.share, child: Text('Share')), 363 + items: [ 364 + PopupMenuItem<_ImageThumbnailAction>( 365 + value: _ImageThumbnailAction.save, 366 + child: Text(context.l10n.labelSaveImage), 367 + ), 368 + PopupMenuItem<_ImageThumbnailAction>(value: _ImageThumbnailAction.share, child: Text(context.l10n.buttonShare)), 365 369 ], 366 370 ); 367 371
+5 -4
lib/features/feed/presentation/widgets/post_interactions_sheet.dart
··· 2 2 import 'package:bluesky/app_bsky_actor_defs.dart'; 3 3 import 'package:flutter/material.dart'; 4 4 import 'package:lazurite/core/cache/lazurite_image_cache.dart'; 5 + import 'package:lazurite/core/l10n/l10n.dart'; 5 6 import 'package:lazurite/core/theme/theme_extensions.dart'; 6 7 import 'package:lazurite/features/feed/data/post_action_repository.dart'; 7 8 import 'package:lazurite/shared/presentation/helpers/navigation_helpers.dart'; ··· 140 141 } 141 142 142 143 Widget _buildSectionLabel(BuildContext context, ColorScheme colorScheme) { 143 - final label = _selectedTab == InteractionTab.likes ? 'LIKED BY' : 'REPOSTED BY'; 144 + final label = _selectedTab == InteractionTab.likes ? context.l10n.labelLikedBy : context.l10n.labelRepostedBy; 144 145 return Padding( 145 146 padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), 146 147 child: Text( ··· 164 165 colorScheme: colorScheme, 165 166 tab: InteractionTab.likes, 166 167 icon: Icons.favorite_outline, 167 - label: '${widget.likeCount} Likes', 168 + label: context.l10n.formatLikesCount(widget.likeCount), 168 169 ), 169 170 const SizedBox(width: 10), 170 171 _buildTabChip( 171 172 colorScheme: colorScheme, 172 173 tab: InteractionTab.reposts, 173 174 icon: Icons.repeat, 174 - label: '${widget.repostCount} Reposts', 175 + label: context.l10n.formatRepostsCount(widget.repostCount), 175 176 ), 176 177 ], 177 178 ), ··· 229 230 230 231 if (loaded && profiles.isEmpty) { 231 232 return Center( 232 - child: Text('No interactions yet', style: TextStyle(color: colorScheme.onSurfaceVariant)), 233 + child: Text(context.l10n.messageNoInteractionsYet, style: TextStyle(color: colorScheme.onSurfaceVariant)), 233 234 ); 234 235 } 235 236
+13 -12
lib/features/feed/presentation/widgets/post_menu_actions.dart
··· 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter_bloc/flutter_bloc.dart'; 4 4 import 'package:flutter/services.dart'; 5 + import 'package:lazurite/core/l10n/l10n.dart'; 5 6 import 'package:lazurite/core/network/app_view_provider.dart'; 6 7 import 'package:lazurite/core/network/app_view_web_links.dart'; 7 8 import 'package:lazurite/features/feed/data/post_action_repository.dart'; ··· 34 35 items: [ 35 36 OptionsSheetItem( 36 37 leading: const Icon(Icons.favorite_outline), 37 - title: 'Show Liked Users', 38 - subtitle: 'View who liked this post', 38 + title: context.l10n.labelShowLikedUsers, 39 + subtitle: context.l10n.messageShowLikedUsersSubtitle, 39 40 enabled: !isOffline, 40 41 onTap: () => showLikedUsersSheet(context: context, post: post, repository: repository), 41 42 ), 42 43 OptionsSheetItem( 43 44 leading: const Icon(Icons.repeat), 44 - title: 'Show Quote/Repost List', 45 - subtitle: 'View quote posts and expand reposts', 45 + title: context.l10n.labelShowQuoteRepostList, 46 + subtitle: context.l10n.messageShowQuoteRepostListSubtitle, 46 47 enabled: !isOffline, 47 48 onTap: () => showQuoteRepostSheet(context: context, post: post, repository: repository), 48 49 ), 49 50 OptionsSheetItem( 50 51 leading: const Icon(Icons.format_quote), 51 - title: 'Quote Post', 52 - subtitle: 'Quote this post with your own text', 52 + title: context.l10n.labelQuotePost, 53 + subtitle: context.l10n.messageQuotePostSubtitle, 53 54 enabled: !isOffline, 54 55 onTap: onQuote, 55 56 ), 56 57 OptionsSheetItem( 57 58 leading: const Icon(Icons.copy), 58 - title: 'Copy Link', 59 + title: context.l10n.labelCopyLink, 59 60 onTap: () => _copyToClipboard(context, bskyUrl), 60 61 ), 61 62 OptionsSheetItem( 62 63 leading: const Icon(Icons.person_outline), 63 - title: 'View @${post.author.handle}', 64 + title: context.l10n.formatViewHandle(post.author.handle), 64 65 onTap: () => navigateToProfile(context, post.author.did), 65 66 ), 66 67 OptionsSheetItem( 67 68 leading: const Icon(Icons.report_outlined, color: Colors.orange), 68 - title: 'Report Post', 69 + title: context.l10n.labelReportPost, 69 70 onTap: onShowReport, 70 71 ), 71 72 if (post.author.did == accountDid && onEdit != null) 72 - OptionsSheetItem(leading: const Icon(Icons.edit_outlined), title: 'Edit Post', onTap: onEdit), 73 + OptionsSheetItem(leading: const Icon(Icons.edit_outlined), title: context.l10n.labelEditPost, onTap: onEdit), 73 74 if (post.author.did == accountDid && onDelete != null) 74 75 OptionsSheetItem( 75 76 leading: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error), 76 - title: 'Delete Post', 77 + title: context.l10n.labelDeletePost, 77 78 isDestructive: true, 78 79 onTap: onDelete, 79 80 ), ··· 118 119 119 120 void _copyToClipboard(BuildContext context, String text) { 120 121 Clipboard.setData(ClipboardData(text: text)); 121 - showAppSnackBar(context, 'Link copied to clipboard', behavior: SnackBarBehavior.floating); 122 + showAppSnackBar(context, context.l10n.messageLinkCopiedToClipboard, behavior: SnackBarBehavior.floating); 122 123 } 123 124 124 125 String _resolveAppViewProvider(BuildContext context) {
+14 -7
lib/features/feed/presentation/widgets/post_quote_repost_sheet.dart
··· 5 5 import 'package:flutter/material.dart'; 6 6 import 'package:go_router/go_router.dart'; 7 7 import 'package:lazurite/core/cache/lazurite_image_cache.dart'; 8 + import 'package:lazurite/core/l10n/l10n.dart'; 8 9 import 'package:lazurite/core/theme/theme_extensions.dart'; 9 10 import 'package:lazurite/features/feed/data/post_action_repository.dart'; 10 11 import 'package:lazurite/shared/presentation/helpers/navigation_helpers.dart'; ··· 113 114 padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), 114 115 children: [ 115 116 Text( 116 - 'QUOTE / REPOSTS', 117 + context.l10n.labelQuoteReposts, 117 118 style: context.textTheme.labelSmall?.copyWith( 118 119 color: colorScheme.onSurfaceVariant, 119 120 fontWeight: FontWeight.w700, ··· 144 145 Icon(Icons.format_quote, size: 16, color: colorScheme.onSurfaceVariant), 145 146 const SizedBox(width: 8), 146 147 Expanded( 147 - child: Text('Quotes', style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w700)), 148 + child: Text( 149 + context.l10n.labelQuotes, 150 + style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w700), 151 + ), 148 152 ), 149 153 Text( 150 154 '${widget.quoteCount}', ··· 162 166 else if (_quotesLoaded && _quotes.isEmpty) 163 167 Padding( 164 168 padding: const EdgeInsets.all(16), 165 - child: Text('No quotes yet', style: TextStyle(color: colorScheme.onSurfaceVariant)), 169 + child: Text(context.l10n.messageNoQuotesYet, style: TextStyle(color: colorScheme.onSurfaceVariant)), 166 170 ) 167 171 else 168 172 Column( ··· 173 177 onPressed: _loadingQuotes ? null : _loadQuotes, 174 178 child: _loadingQuotes 175 179 ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) 176 - : const Text('Load more quotes'), 180 + : Text(context.l10n.buttonLoadMoreQuotes), 177 181 ), 178 182 ], 179 183 ), ··· 193 197 iconColor: colorScheme.onSurfaceVariant, 194 198 collapsedIconColor: colorScheme.onSurfaceVariant, 195 199 leading: Icon(Icons.repeat, color: colorScheme.onSurfaceVariant), 196 - title: Text('Reposts', style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w700)), 200 + title: Text( 201 + context.l10n.labelReposts, 202 + style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w700), 203 + ), 197 204 trailing: Row( 198 205 mainAxisSize: MainAxisSize.min, 199 206 children: [ ··· 212 219 else if (_repostsLoaded && _reposters.isEmpty) 213 220 Padding( 214 221 padding: const EdgeInsets.all(16), 215 - child: Text('No reposts yet', style: TextStyle(color: colorScheme.onSurfaceVariant)), 222 + child: Text(context.l10n.messageNoRepostsYet, style: TextStyle(color: colorScheme.onSurfaceVariant)), 216 223 ) 217 224 else 218 225 Column( ··· 223 230 onPressed: _loadingReposts ? null : _loadReposts, 224 231 child: _loadingReposts 225 232 ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) 226 - : const Text('Load more reposts'), 233 + : Text(context.l10n.buttonLoadMoreReposts), 227 234 ), 228 235 ], 229 236 ),
+24 -23
lib/features/lists/presentation/list_detail_screen.dart
··· 4 4 import 'package:flutter/material.dart'; 5 5 import 'package:flutter_bloc/flutter_bloc.dart'; 6 6 import 'package:go_router/go_router.dart'; 7 + import 'package:lazurite/core/l10n/l10n.dart'; 8 + import 'package:lazurite/core/theme/theme_extensions.dart'; 7 9 import 'package:lazurite/core/widgets/sliver_tab_bar_delegate.dart'; 8 10 import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 9 11 import 'package:lazurite/features/feed/presentation/widgets/post_card_with_actions.dart'; ··· 14 16 import 'package:lazurite/shared/presentation/helpers/navigation_helpers.dart'; 15 17 import 'package:lazurite/shared/presentation/widgets/confirmation_dialog.dart'; 16 18 import 'package:lazurite/shared/presentation/widgets/options_sheet.dart'; 17 - import 'package:lazurite/core/theme/theme_extensions.dart'; 18 19 19 20 class ListDetailScreen extends StatelessWidget { 20 21 const ListDetailScreen({super.key, required this.listUri}); ··· 99 100 Future<void> _confirmDelete(BuildContext context) async { 100 101 final confirmed = await showConfirmationDialog( 101 102 context: context, 102 - title: const Text('Delete list?'), 103 - content: const Text('This action cannot be undone.'), 104 - confirmLabel: 'Delete', 103 + title: Text(context.l10n.dialogDeleteListTitle), 104 + content: Text(context.l10n.dialogDeletePostContent), 105 + confirmLabel: context.l10n.buttonDelete, 105 106 confirmDestructive: true, 106 107 ); 107 108 ··· 127 128 if (isOwn) 128 129 OptionsSheetItem( 129 130 leading: const Icon(Icons.edit_outlined), 130 - title: 'Edit list', 131 + title: context.l10n.labelEditList, 131 132 onTap: () => _showEditDialog(context, list), 132 133 ), 133 134 if (isOwn) 134 135 OptionsSheetItem( 135 136 leading: const Icon(Icons.person_add_outlined), 136 - title: 'Add members', 137 + title: context.l10n.buttonAddMembers, 137 138 onTap: () async { 138 139 final listUriStr = Uri.encodeComponent(list.uri.toString()); 139 140 await context.push('/list/members?uri=$listUriStr'); ··· 145 146 if (isOwn) 146 147 OptionsSheetItem( 147 148 leading: Icon(Icons.delete_outline, color: context.colorScheme.error), 148 - title: 'Delete list', 149 + title: context.l10n.dialogDeleteListTitle, 149 150 isDestructive: true, 150 151 onTap: () => _confirmDelete(context), 151 152 ), 152 153 OptionsSheetItem( 153 154 leading: Icon(isMuted ? Icons.volume_up_outlined : Icons.volume_off_outlined), 154 - title: isMuted ? 'Unmute list' : 'Mute list', 155 + title: isMuted ? context.l10n.labelUnmuteList : context.l10n.labelMuteList, 155 156 onTap: () => context.read<ListBloc>().add(isMuted ? const ListUnmuted() : const ListMuted()), 156 157 ), 157 158 if (list.purpose.knownValue == bsky_graph.KnownListPurpose.appBskyGraphDefsModlist) 158 159 OptionsSheetItem( 159 160 leading: Icon(isBlocked ? Icons.block_flipped : Icons.block_outlined), 160 - title: isBlocked ? 'Unblock via list' : 'Block via list', 161 + title: isBlocked ? context.l10n.labelUnblockViaList : context.l10n.labelBlockViaList, 161 162 onTap: () => context.read<ListBloc>().add(isBlocked ? const ListUnblocked() : const ListBlocked()), 162 163 ), 163 164 ], ··· 188 189 floating: true, 189 190 pinned: true, 190 191 snap: true, 191 - title: Text(list?.name ?? 'List'), 192 + title: Text(list?.name ?? context.l10n.labelList), 192 193 actions: [ 193 194 if (state.isMutating) 194 195 const Padding( ··· 215 216 SliverToBoxAdapter( 216 217 child: Padding( 217 218 padding: const EdgeInsets.all(24), 218 - child: Center(child: Text(state.errorMessage ?? 'Failed to load list')), 219 + child: Center(child: Text(state.errorMessage ?? context.l10n.errorFailedToLoadList)), 219 220 ), 220 221 ), 221 222 SliverPersistentHeader( ··· 223 224 delegate: SliverTabBarDelegate( 224 225 TabBar( 225 226 controller: _tabController, 226 - tabs: const [ 227 - Tab(text: 'FEED'), 228 - Tab(text: 'MEMBERS'), 227 + tabs: [ 228 + Tab(text: context.l10n.labelFeed.toUpperCase()), 229 + Tab(text: context.l10n.labelMembers.toUpperCase()), 229 230 ], 230 231 labelStyle: const TextStyle(fontSize: 11, fontWeight: FontWeight.w700, letterSpacing: 2.2), 231 232 unselectedLabelStyle: const TextStyle( ··· 242 243 body: TabBarView( 243 244 controller: _tabController, 244 245 children: [ 245 - isCuration ? _buildFeedTab(context) : _buildFeedUnavailableTab(), 246 + isCuration ? _buildFeedTab(context) : _buildFeedUnavailableTab(context), 246 247 _buildMembersTab(context, state), 247 248 ], 248 249 ), ··· 278 279 Text(list.name, style: textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700)), 279 280 const SizedBox(height: 2), 280 281 Text( 281 - 'by @${list.creator.handle}', 282 + context.l10n.formatListByHandle(list.creator.handle), 282 283 style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), 283 284 ), 284 285 const SizedBox(height: 4), 285 286 Text( 286 - '${list.listItemCount ?? 0} members', 287 + context.l10n.formatMemberCount(list.listItemCount ?? 0), 287 288 style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), 288 289 ), 289 290 ], ··· 308 309 } 309 310 310 311 if (feedState.hasError) { 311 - return Center(child: Text(feedState.errorMessage ?? 'Failed to load feed')); 312 + return Center(child: Text(feedState.errorMessage ?? context.l10n.errorFailedToLoadFeed)); 312 313 } 313 314 314 315 if (!feedState.hasPosts) { 315 - return const Center(child: Text('No posts yet')); 316 + return Center(child: Text(context.l10n.messageNoPostsYet)); 316 317 } 317 318 318 319 final accountDid = context.read<AuthBloc>().state.tokens?.did ?? ''; ··· 350 351 ); 351 352 } 352 353 353 - Widget _buildFeedUnavailableTab() { 354 - return const Center(child: Text('Feed not available for moderation lists')); 354 + Widget _buildFeedUnavailableTab(BuildContext context) { 355 + return Center(child: Text(context.l10n.messageFeedUnavailableForModerationLists)); 355 356 } 356 357 357 358 Widget _buildMembersTab(BuildContext context, ListState state) { ··· 360 361 } 361 362 362 363 if (state.hasError && !state.hasItems) { 363 - return Center(child: Text(state.errorMessage ?? 'Failed to load members')); 364 + return Center(child: Text(state.errorMessage ?? context.l10n.errorFailedToLoadMembers)); 364 365 } 365 366 366 367 if (!state.hasItems) { 367 - return const Center(child: Text('No members yet')); 368 + return Center(child: Text(context.l10n.messageNoMembersYet)); 368 369 } 369 370 370 371 final isOwn = _isOwnList(context, state.list);
+6 -5
lib/features/lists/presentation/list_members_screen.dart
··· 2 2 import 'package:bluesky/app_bsky_actor_defs.dart'; 3 3 import 'package:flutter/material.dart'; 4 4 import 'package:flutter_bloc/flutter_bloc.dart'; 5 + import 'package:lazurite/core/l10n/l10n.dart'; 6 + import 'package:lazurite/core/theme/theme_extensions.dart'; 5 7 import 'package:lazurite/features/lists/bloc/list_bloc.dart'; 6 8 import 'package:lazurite/features/typeahead/data/typeahead_repository.dart'; 7 9 import 'package:lazurite/shared/presentation/helpers/navigation_helpers.dart'; 8 10 import 'package:lazurite/shared/presentation/widgets/staggered_entrance.dart'; 9 - import 'package:lazurite/core/theme/theme_extensions.dart'; 10 11 11 12 /// Screen for adding and removing members from a list. 12 13 /// ··· 67 68 @override 68 69 Widget build(BuildContext context) { 69 70 return Scaffold( 70 - appBar: AppBar(title: const Text('Add members')), 71 + appBar: AppBar(title: Text(context.l10n.buttonAddMembers)), 71 72 body: BlocBuilder<ListBloc, ListState>( 72 73 builder: (context, state) { 73 74 return Column( ··· 77 78 child: TextField( 78 79 controller: _searchController, 79 80 decoration: InputDecoration( 80 - hintText: 'Search for people', 81 + hintText: context.l10n.messageSearchForPeoplePlaceholder, 81 82 prefixIcon: const Icon(Icons.search), 82 83 suffixIcon: _isSearching 83 84 ? const Padding( ··· 162 163 } 163 164 164 165 if (!state.hasItems) { 165 - return const Center(child: Text('No members yet. Search above to add people.')); 166 + return Center(child: Text(context.l10n.messageNoMembersYetSearch)); 166 167 } 167 168 168 169 final colorScheme = context.colorScheme; ··· 173 174 Padding( 174 175 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 175 176 child: Text( 176 - 'CURRENT MEMBERS', 177 + context.l10n.labelCurrentMembers.toUpperCase(), 177 178 style: TextStyle( 178 179 fontSize: 11, 179 180 fontWeight: FontWeight.w700,
+8 -7
lib/features/lists/presentation/my_lists_screen.dart
··· 3 3 import 'package:flutter_animate/flutter_animate.dart'; 4 4 import 'package:flutter_bloc/flutter_bloc.dart'; 5 5 import 'package:go_router/go_router.dart'; 6 + import 'package:lazurite/core/l10n/l10n.dart'; 6 7 import 'package:lazurite/core/theme/animation_tokens.dart'; 7 8 import 'package:lazurite/core/theme/animation_utils.dart'; 8 9 import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; ··· 82 83 Widget build(BuildContext context) { 83 84 return Scaffold( 84 85 appBar: AppBar( 85 - title: const Text('My Lists'), 86 + title: Text(context.l10n.labelMyLists), 86 87 bottom: TabBar( 87 88 controller: _tabController, 88 - tabs: const [ 89 - Tab(text: 'FEEDS'), 90 - Tab(text: 'MODERATION'), 89 + tabs: [ 90 + Tab(text: context.l10n.labelFeeds.toUpperCase()), 91 + Tab(text: context.l10n.labelModeration.toUpperCase()), 91 92 ], 92 93 labelStyle: const TextStyle(fontSize: 11, fontWeight: FontWeight.w700, letterSpacing: 2.2), 93 94 unselectedLabelStyle: const TextStyle(fontSize: 11, fontWeight: FontWeight.w700, letterSpacing: 2.2), ··· 102 103 103 104 if (state.status == MyListsStatus.error) { 104 105 return ErrorState( 105 - title: 'Failed to load lists', 106 - message: state.errorMessage ?? 'Unknown error', 106 + title: context.l10n.errorFailedToLoadLists, 107 + message: state.errorMessage ?? context.l10n.errorUnknown, 107 108 onRetry: () => context.read<MyListsCubit>().refresh(), 108 109 ); 109 110 } ··· 131 132 132 133 Widget _buildListTab(BuildContext context, List<bsky_graph.ListView> lists) { 133 134 if (lists.isEmpty) { 134 - return const EmptyState(message: 'No lists yet', icon: Icons.list_alt_outlined); 135 + return EmptyState(message: context.l10n.messageNoListsYet, icon: Icons.list_alt_outlined); 135 136 } 136 137 137 138 return AnimatedRefreshIndicator(
+15 -11
lib/features/lists/presentation/widgets/create_edit_list_dialog.dart
··· 1 1 import 'dart:io'; 2 2 import 'dart:typed_data'; 3 + import 'package:lazurite/core/l10n/l10n.dart'; 3 4 import 'package:lazurite/core/theme/theme_extensions.dart'; 4 5 5 6 import 'package:bluesky/app_bsky_graph_defs.dart' show KnownListPurpose; ··· 130 131 final hasAvatar = _avatarBytes != null || widget.initialAvatarUrl != null; 131 132 132 133 return AlertDialog( 133 - title: Text(_isEditing ? 'Edit list' : 'Create list'), 134 + title: Text(_isEditing ? context.l10n.labelEditList : context.l10n.labelCreateList), 134 135 content: SizedBox( 135 136 width: double.maxFinite, 136 137 child: SingleChildScrollView( ··· 176 177 const SizedBox(height: 16), 177 178 TextField( 178 179 controller: _nameController, 179 - decoration: const InputDecoration(labelText: 'Name', border: OutlineInputBorder()), 180 + decoration: InputDecoration(labelText: context.l10n.labelName, border: const OutlineInputBorder()), 180 181 maxLength: 64, 181 182 textCapitalization: TextCapitalization.sentences, 182 183 ), 183 184 const SizedBox(height: 12), 184 185 TextField( 185 186 controller: _descController, 186 - decoration: const InputDecoration(labelText: 'Description (optional)', border: OutlineInputBorder()), 187 + decoration: InputDecoration( 188 + labelText: context.l10n.labelDescriptionOptional, 189 + border: const OutlineInputBorder(), 190 + ), 187 191 maxLength: 300, 188 192 maxLines: 3, 189 193 textCapitalization: TextCapitalization.sentences, 190 194 ), 191 195 if (!_isEditing) ...[ 192 196 const SizedBox(height: 12), 193 - Text('Type', style: context.textTheme.labelLarge), 197 + Text(context.l10n.labelType, style: context.textTheme.labelLarge), 194 198 const SizedBox(height: 8), 195 199 SegmentedButton<KnownListPurpose>( 196 - segments: const [ 200 + segments: [ 197 201 ButtonSegment( 198 202 value: KnownListPurpose.appBskyGraphDefsCuratelist, 199 - label: Text('Feed'), 200 - icon: Icon(Icons.dynamic_feed_outlined), 203 + label: Text(context.l10n.labelFeed), 204 + icon: const Icon(Icons.dynamic_feed_outlined), 201 205 ), 202 206 ButtonSegment( 203 207 value: KnownListPurpose.appBskyGraphDefsModlist, 204 - label: Text('Moderation'), 205 - icon: Icon(Icons.shield_outlined), 208 + label: Text(context.l10n.labelModeration), 209 + icon: const Icon(Icons.shield_outlined), 206 210 ), 207 211 ], 208 212 selected: {_purpose}, ··· 214 218 ), 215 219 ), 216 220 actions: [ 217 - TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), 221 + TextButton(onPressed: () => Navigator.pop(context), child: Text(context.l10n.buttonCancel)), 218 222 FilledButton( 219 223 onPressed: _nameController.text.trim().isEmpty ? null : _save, 220 - child: Text(_isEditing ? 'Save' : 'Create'), 224 + child: Text(_isEditing ? context.l10n.buttonSave : context.l10n.buttonCreate), 221 225 ), 222 226 ], 223 227 );
+6 -2
lib/features/lists/presentation/widgets/list_row_tile.dart
··· 1 1 import 'package:bluesky/app_bsky_graph_defs.dart' as bsky_graph; 2 2 import 'package:flutter/material.dart'; 3 + import 'package:lazurite/core/l10n/l10n.dart'; 3 4 import 'package:lazurite/core/theme/theme_extensions.dart'; 4 5 import 'package:lazurite/shared/presentation/widgets/profile_avatar.dart'; 5 6 ··· 15 16 Widget build(BuildContext context) { 16 17 final colorScheme = context.colorScheme; 17 18 final isMod = list.purpose.knownValue == bsky_graph.KnownListPurpose.appBskyGraphDefsModlist; 18 - final purposeLabel = isMod ? 'MOD' : 'FEED'; 19 + final purposeLabel = isMod ? context.l10n.labelModerationShort : context.l10n.labelFeed.toUpperCase(); 19 20 final purposeColor = isMod ? colorScheme.error : colorScheme.primary; 20 21 21 22 return ListTile( ··· 27 28 fallbackBuilder: (_) => Icon(Icons.list, color: colorScheme.onSurfaceVariant), 28 29 ), 29 30 title: Text(list.name, maxLines: 1, overflow: TextOverflow.ellipsis), 30 - subtitle: Text('${list.listItemCount ?? 0} members', style: TextStyle(color: colorScheme.onSurfaceVariant)), 31 + subtitle: Text( 32 + context.l10n.formatMemberCount(list.listItemCount ?? 0), 33 + style: TextStyle(color: colorScheme.onSurfaceVariant), 34 + ), 31 35 trailing: 32 36 trailing ?? 33 37 Container(
+47 -26
lib/features/logs/presentation/logs_screen.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:flutter_bloc/flutter_bloc.dart'; 3 3 import 'package:logger/logger.dart'; 4 + import 'package:lazurite/core/l10n/l10n.dart'; 4 5 import 'package:lazurite/core/logging/app_logger.dart'; 6 + import 'package:lazurite/core/theme/theme_extensions.dart'; 5 7 import 'package:lazurite/features/logs/cubit/log_viewer_cubit.dart'; 6 8 import 'package:lazurite/features/logs/data/log_entry.dart'; 7 9 import 'package:lazurite/shared/presentation/helpers/share_helper.dart'; 8 10 import 'package:lazurite/shared/presentation/widgets/empty_state.dart'; 9 11 import 'package:lazurite/shared/presentation/widgets/error_state.dart'; 10 12 import 'package:lazurite/shared/presentation/widgets/loading_state.dart'; 11 - import 'package:lazurite/core/theme/theme_extensions.dart'; 12 13 13 14 class LogsScreen extends StatelessWidget { 14 15 const LogsScreen({super.key}); ··· 71 72 72 73 @override 73 74 Widget build(BuildContext context) { 75 + final l10n = context.l10n; 76 + 74 77 return BlocListener<LogViewerCubit, LogViewerState>( 75 78 listenWhen: (previous, current) => 76 79 previous.filteredEntries != current.filteredEntries || previous.status != current.status, ··· 83 86 }, 84 87 child: Scaffold( 85 88 appBar: AppBar( 86 - title: const Text('Logs'), 89 + title: Text(l10n.labelLogs), 87 90 actions: [ 88 - IconButton(icon: const Icon(Icons.share_outlined), tooltip: 'Share log file', onPressed: _shareLogs), 91 + IconButton( 92 + icon: const Icon(Icons.share_outlined), 93 + tooltip: l10n.tooltipShareLogFile, 94 + onPressed: _shareLogs, 95 + ), 89 96 IconButton( 90 97 icon: Icon(Icons.delete_outline, color: context.colorScheme.error), 91 - tooltip: 'Clear all logs', 98 + tooltip: l10n.tooltipClearAllLogs, 92 99 onPressed: () => _confirmClearLogs(context), 93 100 ), 94 101 ], ··· 115 122 final cubit = context.read<LogViewerCubit>(); 116 123 final messenger = ScaffoldMessenger.of(context); 117 124 final shareOrigin = ShareHelper.sharePositionOriginForContext(context); 125 + final l10n = context.l10n; 118 126 119 127 final file = await cubit.getTodaysLogFile(); 120 128 if (!mounted) { ··· 125 133 if (!mounted) { 126 134 return; 127 135 } 128 - messenger.showSnackBar(const SnackBar(content: Text('No log file available'))); 136 + messenger.showSnackBar(SnackBar(content: Text(l10n.messageNoLogFileAvailable))); 129 137 return; 130 138 } 131 139 ··· 134 142 } 135 143 136 144 try { 137 - await ShareHelper.shareFilePathsAtOrigin(shareOrigin, [file.path], subject: 'Lazurite logs'); 145 + await ShareHelper.shareFilePathsAtOrigin(shareOrigin, [file.path], subject: l10n.subjectLazuriteLogs); 138 146 } catch (error, stackTrace) { 139 147 log.e('LogsScreen: Failed to open share sheet for log file', error: error, stackTrace: stackTrace); 140 148 if (!mounted) { 141 149 return; 142 150 } 143 - messenger.showSnackBar(const SnackBar(content: Text('Unable to open share sheet. Please try again.'))); 151 + messenger.showSnackBar(SnackBar(content: Text(l10n.messageUnableToOpenShareSheet))); 144 152 } 145 153 } 146 154 ··· 148 156 final confirmed = await showDialog<bool>( 149 157 context: context, 150 158 builder: (context) => AlertDialog( 151 - title: const Text('Clear all logs?'), 152 - content: const Text('This will permanently delete all log files. This action cannot be undone.'), 159 + title: Text(context.l10n.dialogClearAllLogsTitle), 160 + content: Text(context.l10n.dialogClearAllLogsContent), 153 161 actions: [ 154 - TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), 162 + TextButton(onPressed: () => Navigator.pop(context, false), child: Text(context.l10n.buttonCancel)), 155 163 TextButton( 156 164 onPressed: () => Navigator.pop(context, true), 157 - child: Text('Clear', style: TextStyle(color: context.colorScheme.error)), 165 + child: Text(context.l10n.buttonClear, style: TextStyle(color: context.colorScheme.error)), 158 166 ), 159 167 ], 160 168 ), ··· 196 204 ), 197 205 const SizedBox(width: 4), 198 206 Text( 199 - 'Auto-scroll', 207 + context.l10n.labelAutoScroll, 200 208 style: TextStyle(fontSize: 12, color: isActive ? colorScheme.primary : colorScheme.outline), 201 209 ), 202 210 ], ··· 214 222 padding: const EdgeInsets.all(12), 215 223 child: TextField( 216 224 decoration: InputDecoration( 217 - hintText: 'Filter logs...', 225 + hintText: context.l10n.placeholderLogsFilter, 218 226 prefixIcon: const Icon(Icons.search, size: 20), 219 227 border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), 220 228 contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ··· 229 237 230 238 class _LevelFilterChips extends StatelessWidget { 231 239 static const _levels = [ 232 - (Level.fatal, 'Fatal', Colors.orange), 233 - (Level.error, 'Error', Colors.red), 234 - (Level.warning, 'Warning', Colors.yellow), 235 - (Level.info, 'Info', Colors.blue), 236 - (Level.debug, 'Debug', Colors.green), 237 - (Level.trace, 'Trace', Colors.purple), 240 + (Level.fatal, Colors.orange), 241 + (Level.error, Colors.red), 242 + (Level.warning, Colors.yellow), 243 + (Level.info, Colors.blue), 244 + (Level.debug, Colors.green), 245 + (Level.trace, Colors.purple), 238 246 ]; 239 247 240 248 @override ··· 250 258 itemCount: _levels.length, 251 259 separatorBuilder: (context, index) => const SizedBox(width: 6), 252 260 itemBuilder: (context, index) { 253 - final (level, label, color) = _levels[index]; 261 + final (level, color) = _levels[index]; 254 262 final isEnabled = state.enabledLevels.contains(level); 255 263 return FilterChip( 256 - label: Text(label), 264 + label: Text(_labelForLevel(context, level)), 257 265 selected: isEnabled, 258 266 onSelected: (selected) => context.read<LogViewerCubit>().toggleLevel(level), 259 267 avatar: Container( ··· 294 302 295 303 if (state.status == LogViewerStatus.error) { 296 304 return ErrorState( 297 - title: 'Failed to load logs', 298 - message: state.errorMessage ?? 'Unknown error', 305 + title: context.l10n.errorFailedToLoadLogs, 306 + message: state.errorMessage ?? context.l10n.errorUnknown, 299 307 onRetry: () => context.read<LogViewerCubit>().loadLogs(), 300 308 ); 301 309 } 302 310 303 311 if (state.filteredEntries.isEmpty) { 304 - return const EmptyState( 305 - message: 'No logs yet', 306 - subtitle: 'Log entries will appear here', 312 + return EmptyState( 313 + message: context.l10n.messageLogsEmpty, 314 + subtitle: context.l10n.messageLogsEmptySubtitle, 307 315 icon: Icons.description_outlined, 308 316 ); 309 317 } ··· 319 327 }, 320 328 ); 321 329 } 330 + } 331 + 332 + String _labelForLevel(BuildContext context, Level level) { 333 + final l10n = context.l10n; 334 + return switch (level) { 335 + Level.fatal => l10n.labelLogLevelFatal, 336 + Level.error => l10n.labelLogLevelError, 337 + Level.warning => l10n.labelLogLevelWarning, 338 + Level.info => l10n.labelLogLevelInfo, 339 + Level.debug => l10n.labelLogLevelDebug, 340 + Level.trace => l10n.labelLogLevelTrace, 341 + _ => level.name, 342 + }; 322 343 } 323 344 324 345 class _LogEntryTile extends StatefulWidget {
+6 -5
lib/features/messages/presentation/convo_list_screen.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:flutter_bloc/flutter_bloc.dart'; 3 + import 'package:lazurite/core/l10n/l10n.dart'; 3 4 import 'package:lazurite/core/router/app_shell.dart'; 5 + import 'package:lazurite/core/theme/theme_extensions.dart'; 4 6 import 'package:lazurite/features/messages/bloc/convo_list_bloc.dart'; 5 7 import 'package:lazurite/features/messages/presentation/widgets/convo_list_pane.dart'; 6 - import 'package:lazurite/core/theme/theme_extensions.dart'; 7 8 8 9 class ConvoListScreen extends StatefulWidget { 9 10 const ConvoListScreen({super.key}); ··· 41 42 return Scaffold( 42 43 appBar: AppBar( 43 44 leading: const AppShellMenuButton(), 44 - title: Text('Messages', style: context.textTheme.titleMedium), 45 + title: Text(context.l10n.labelMessages, style: context.textTheme.titleMedium), 45 46 bottom: TabBar( 46 47 controller: _tabController, 47 - tabs: const [ 48 - Tab(text: 'Primary'), 49 - Tab(text: 'Requests'), 48 + tabs: [ 49 + Tab(text: context.l10n.labelPrimary), 50 + Tab(text: context.l10n.labelMessageRequests), 50 51 ], 51 52 ), 52 53 ),
+9 -6
lib/features/messages/presentation/message_thread_screen.dart
··· 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter/services.dart'; 4 4 import 'package:flutter_bloc/flutter_bloc.dart'; 5 + import 'package:lazurite/core/l10n/l10n.dart'; 5 6 import 'package:lazurite/features/messages/bloc/convo_list_bloc.dart'; 6 7 import 'package:lazurite/features/messages/bloc/message_bloc.dart'; 7 8 import 'package:lazurite/features/messages/presentation/widgets/message_bubble.dart'; ··· 66 67 Clipboard.setData(ClipboardData(text: lines)); 67 68 ScaffoldMessenger.of( 68 69 context, 69 - ).showSnackBar(const SnackBar(content: Text('Thread copied'), duration: Duration(seconds: 2))); 70 + ).showSnackBar(SnackBar(content: Text(context.l10n.messageThreadCopied), duration: const Duration(seconds: 2))); 70 71 } 71 72 72 73 ThemeData get _theme => Theme.of(context); ··· 86 87 _copyAllMessages(state.messages); 87 88 } 88 89 }, 89 - itemBuilder: (_) => const [PopupMenuItem(value: _ThreadAction.copyAll, child: Text('Copy All'))], 90 + itemBuilder: (_) => [ 91 + PopupMenuItem(value: _ThreadAction.copyAll, child: Text(context.l10n.buttonCopyAll)), 92 + ], 90 93 ), 91 94 ), 92 95 ], ··· 106 109 } 107 110 108 111 if (state.messages.isEmpty) { 109 - return Center(child: Text('No messages yet', style: _theme.textTheme.bodyLarge)); 112 + return Center(child: Text(context.l10n.messageNoMessagesYet, style: _theme.textTheme.bodyLarge)); 110 113 } 111 114 112 115 return ListView.builder( ··· 157 160 maxLines: 5, 158 161 textCapitalization: TextCapitalization.sentences, 159 162 decoration: InputDecoration( 160 - hintText: 'Message…', 163 + hintText: context.l10n.messagePlaceholder, 161 164 border: OutlineInputBorder(borderRadius: BorderRadius.circular(24)), 162 165 contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), 163 166 isDense: true, ··· 183 186 child: Column( 184 187 mainAxisAlignment: MainAxisAlignment.center, 185 188 children: [ 186 - Text('Failed to load messages', style: _theme.textTheme.titleMedium), 189 + Text(context.l10n.errorFailedToLoadMessages, style: _theme.textTheme.titleMedium), 187 190 const SizedBox(height: 16), 188 191 FilledButton( 189 192 onPressed: () => context.read<MessageBloc>().add(MessagesRequested(convoId: widget.convoId)), 190 - child: const Text('Retry'), 193 + child: Text(context.l10n.buttonRetry), 191 194 ), 192 195 ], 193 196 ),
+6 -5
lib/features/messages/presentation/widgets/convo_list_item.dart
··· 1 1 import 'package:bluesky/chat_bsky_convo_defs.dart'; 2 2 import 'package:flutter/material.dart'; 3 + import 'package:lazurite/core/l10n/l10n.dart'; 3 4 import 'package:lazurite/core/theme/theme_extensions.dart'; 4 5 import 'package:lazurite/shared/presentation/widgets/actor_name_widget.dart'; 5 6 import 'package:lazurite/shared/presentation/widgets/profile_avatar.dart'; ··· 23 24 final theme = Theme.of(context); 24 25 final other = convo.members.where((m) => m.did != currentUserDid).firstOrNull; 25 26 final displayName = other?.displayName; 26 - final handle = other?.handle ?? 'unknown'; 27 + final handle = other?.handle ?? context.l10n.commonUnknown; 27 28 final fallbackName = displayName ?? handle; 28 - final lastMessageText = _lastMessageText(); 29 + final lastMessageText = _lastMessageText(context); 29 30 30 31 return ListTile( 31 32 onTap: onTap, ··· 78 79 itemBuilder: (_) => [ 79 80 PopupMenuItem( 80 81 value: convo.muted ? _ConvoAction.unmute : _ConvoAction.mute, 81 - child: Text(convo.muted ? 'Unmute' : 'Mute'), 82 + child: Text(convo.muted ? context.l10n.buttonUnmute : context.l10n.buttonMute), 82 83 ), 83 84 ], 84 85 child: Icon(Icons.more_vert, color: theme.colorScheme.onSurfaceVariant), ··· 95 96 ); 96 97 } 97 98 98 - String? _lastMessageText() { 99 + String? _lastMessageText(BuildContext context) { 99 100 final lastMessage = convo.lastMessage; 100 101 if (lastMessage == null) return null; 101 102 102 103 return lastMessage.when( 103 104 messageView: (data) => data.text.isNotEmpty ? data.text : null, 104 - deletedMessageView: (_) => 'Message deleted', 105 + deletedMessageView: (_) => context.l10n.messageDeleted, 105 106 unknown: (_) => null, 106 107 ); 107 108 }
+14 -7
lib/features/messages/presentation/widgets/convo_list_pane.dart
··· 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter_bloc/flutter_bloc.dart'; 4 4 import 'package:go_router/go_router.dart'; 5 + import 'package:lazurite/core/l10n/l10n.dart'; 5 6 import 'package:lazurite/core/logging/app_logger.dart'; 7 + import 'package:lazurite/core/theme/theme_extensions.dart'; 6 8 import 'package:lazurite/features/connectivity/cubit/connectivity_cubit.dart'; 7 9 import 'package:lazurite/features/messages/bloc/convo_list_bloc.dart'; 8 10 import 'package:lazurite/features/messages/presentation/message_thread_route_args.dart'; ··· 12 14 import 'package:lazurite/shared/presentation/widgets/error_state.dart'; 13 15 import 'package:lazurite/shared/presentation/widgets/loading_state.dart'; 14 16 import 'package:lazurite/shared/presentation/widgets/staggered_entrance.dart'; 15 - import 'package:lazurite/core/theme/theme_extensions.dart'; 16 17 17 18 class ConvoListPane extends StatefulWidget { 18 19 const ConvoListPane({super.key, required this.tab}); ··· 93 94 return const _OfflineConvoState(); 94 95 } 95 96 return ErrorState( 96 - title: 'Failed to load messages', 97 - message: state.errorMessage ?? 'Unknown error', 97 + title: context.l10n.errorFailedToLoadMessages, 98 + message: state.errorMessage ?? context.l10n.errorUnknown, 98 99 onRetry: () => context.read<ConvoListBloc>().add(const ConvosRequested()), 99 100 ); 100 101 } ··· 113 114 SizedBox( 114 115 height: MediaQuery.of(context).size.height * 0.5, 115 116 child: EmptyState( 116 - message: widget.tab == ConvoTab.primary ? 'No conversations yet' : 'No message requests', 117 + message: widget.tab == ConvoTab.primary 118 + ? context.l10n.messageNoConversationsYet 119 + : context.l10n.messageNoMessageRequests, 117 120 icon: Icons.forum_outlined, 118 121 ), 119 122 ), ··· 162 165 163 166 void _openThread(BuildContext context, ConvoView convo, String currentUserDid) { 164 167 final other = convo.members.where((m) => m.did != currentUserDid).firstOrNull; 165 - final title = other?.displayName ?? other?.handle ?? 'Conversation'; 168 + final title = other?.displayName ?? other?.handle ?? context.l10n.labelConversation; 166 169 context.push('/alerts/messages/${convo.id}', extra: MessageThreadRouteArgs(title: title)); 167 170 } 168 171 } ··· 180 183 children: [ 181 184 Icon(Icons.cloud_off_outlined, size: 48, color: context.colorScheme.outline), 182 185 const SizedBox(height: 12), 183 - Text('No connection', style: context.textTheme.titleMedium), 186 + Text(context.l10n.messageNoConnection, style: context.textTheme.titleMedium), 184 187 const SizedBox(height: 8), 185 - Text('Reconnect to load messages.', textAlign: TextAlign.center, style: context.textTheme.bodyMedium), 188 + Text( 189 + context.l10n.messageReconnectToLoadMessages, 190 + textAlign: TextAlign.center, 191 + style: context.textTheme.bodyMedium, 192 + ), 186 193 ], 187 194 ), 188 195 ),
+3 -2
lib/features/messages/presentation/widgets/message_bubble.dart
··· 1 1 import 'package:bluesky/chat_bsky_convo_defs.dart'; 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter/services.dart'; 4 + import 'package:lazurite/core/l10n/l10n.dart'; 4 5 5 6 class MessageBubble extends StatelessWidget { 6 7 const MessageBubble({super.key, required this.message, required this.isCurrentUser}); ··· 48 49 Clipboard.setData(ClipboardData(text: message.text)); 49 50 ScaffoldMessenger.of( 50 51 context, 51 - ).showSnackBar(const SnackBar(content: Text('Message copied'), duration: Duration(seconds: 2))); 52 + ).showSnackBar(SnackBar(content: Text(context.l10n.messageCopied), duration: const Duration(seconds: 2))); 52 53 } 53 54 } 54 55 ··· 77 78 ), 78 79 ), 79 80 child: Text( 80 - 'Message deleted', 81 + context.l10n.messageDeleted, 81 82 style: theme.textTheme.bodyMedium?.copyWith( 82 83 color: theme.colorScheme.onSurfaceVariant, 83 84 fontStyle: FontStyle.italic,
+42 -19
lib/features/moderation/presentation/moderation_ui_helpers.dart
··· 3 3 import 'package:bluesky/moderation.dart' as bsky_moderation; 4 4 import 'package:flutter/material.dart'; 5 5 import 'package:flutter_bloc/flutter_bloc.dart'; 6 + import 'package:lazurite/core/l10n/app_localizations.dart'; 6 7 import 'package:lazurite/features/moderation/data/moderation_service.dart'; 7 8 8 9 const officialBlueskyLabelerDid = 'did:plc:ar7c4by46qjdydhdevvrndac'; ··· 74 75 List<ModerationBadgeDescriptor> moderationBadgesForUi( 75 76 bsky_moderation.ModerationUI ui, { 76 77 ModerationLabelResolver? labelResolver, 78 + AppLocalizations? l10n, 77 79 }) { 78 80 final badges = <ModerationBadgeDescriptor>[]; 79 81 final seen = <String>{}; 80 82 81 83 void addDescriptors(List<bsky_moderation.ModerationCause> causes, ModerationBadgeTone tone) { 82 84 for (final cause in causes) { 83 - final descriptor = moderationDescriptorForCause(cause, tone: tone, labelResolver: labelResolver); 85 + final descriptor = moderationDescriptorForCause(cause, tone: tone, labelResolver: labelResolver, l10n: l10n); 84 86 final key = '${tone.name}:${descriptor.label}:${descriptor.description}'; 85 87 if (seen.add(key)) { 86 88 badges.add(descriptor); ··· 93 95 return badges; 94 96 } 95 97 96 - List<String> moderationBlurLabels(bsky_moderation.ModerationUI ui, {ModerationLabelResolver? labelResolver}) { 98 + List<String> moderationBlurLabels( 99 + bsky_moderation.ModerationUI ui, { 100 + ModerationLabelResolver? labelResolver, 101 + AppLocalizations? l10n, 102 + }) { 97 103 final labels = <String>[]; 98 104 final seen = <String>{}; 99 105 ··· 102 108 cause, 103 109 tone: ModerationBadgeTone.alert, 104 110 labelResolver: labelResolver, 111 + l10n: l10n, 105 112 ); 106 113 if (seen.add(descriptor.label)) { 107 114 labels.add(descriptor.label); ··· 115 122 bsky_moderation.ModerationCause cause, { 116 123 required ModerationBadgeTone tone, 117 124 ModerationLabelResolver? labelResolver, 125 + AppLocalizations? l10n, 118 126 }) { 119 127 return cause.maybeWhen( 120 128 label: (data) { ··· 125 133 final label = (resolvedLabel == null || resolvedLabel.isEmpty) 126 134 ? humanizeModerationLabel(data.labelDef.identifier) 127 135 : resolvedLabel; 128 - final source = data.labelDef.definedBy == officialBlueskyLabelerDid ? 'Bluesky' : 'Subscribed labeler'; 129 - return ModerationBadgeDescriptor(label: label, description: '$source label', tone: tone); 136 + final source = data.labelDef.definedBy == officialBlueskyLabelerDid 137 + ? (l10n?.labelModerationSourceBluesky ?? 'Bluesky') 138 + : (l10n?.labelModerationSourceSubscribedLabeler ?? 'Subscribed labeler'); 139 + return ModerationBadgeDescriptor( 140 + label: label, 141 + description: l10n?.formatModerationSourceLabel(source) ?? '$source label', 142 + tone: tone, 143 + ); 130 144 }, 131 145 muted: (_) => ModerationBadgeDescriptor( 132 - label: 'Muted account', 133 - description: 'Muted content is being downranked here', 146 + label: l10n?.labelMutedAccount ?? 'Muted account', 147 + description: l10n?.messageMutedAccountDescription ?? 'Muted content is being downranked here', 134 148 tone: tone, 135 149 ), 136 150 muteWord: (_) => ModerationBadgeDescriptor( 137 - label: 'Muted phrase', 138 - description: 'A muted phrase matched this content', 151 + label: l10n?.labelMutedPhrase ?? 'Muted phrase', 152 + description: l10n?.messageMutedPhraseDescription ?? 'A muted phrase matched this content', 153 + tone: tone, 154 + ), 155 + blocking: (_) => ModerationBadgeDescriptor( 156 + label: l10n?.labelBlockedAccount ?? 'Blocked account', 157 + description: l10n?.messageBlockedAccountDescription ?? 'This account is blocked', 158 + tone: tone, 159 + ), 160 + blockedBy: (_) => ModerationBadgeDescriptor( 161 + label: l10n?.labelBlockedByAccount ?? 'Blocked by account', 162 + description: l10n?.messageBlockedByAccountDescription ?? 'This account has blocked you', 139 163 tone: tone, 140 164 ), 141 - blocking: (_) => 142 - ModerationBadgeDescriptor(label: 'Blocked account', description: 'This account is blocked', tone: tone), 143 - blockedBy: (_) => 144 - ModerationBadgeDescriptor(label: 'Blocked by account', description: 'This account has blocked you', tone: tone), 145 165 blockOther: (_) => ModerationBadgeDescriptor( 146 - label: 'Blocked relationship', 147 - description: 'This content is limited by a block relationship', 166 + label: l10n?.labelBlockedRelationship ?? 'Blocked relationship', 167 + description: l10n?.messageBlockedRelationshipDescription ?? 'This content is limited by a block relationship', 148 168 tone: tone, 149 169 ), 150 170 hidden: (_) => ModerationBadgeDescriptor( 151 - label: 'Hidden content', 152 - description: 'This content is hidden by moderation rules', 171 + label: l10n?.labelHiddenContent ?? 'Hidden content', 172 + description: l10n?.messageHiddenContentDescription ?? 'This content is hidden by moderation rules', 153 173 tone: tone, 154 174 ), 155 175 orElse: () => ModerationBadgeDescriptor( 156 - label: tone == ModerationBadgeTone.alert ? 'Sensitive content' : 'Moderation note', 157 - description: 'Moderation guidance applies here', 176 + label: tone == ModerationBadgeTone.alert 177 + ? (l10n?.labelSensitiveContent ?? 'Sensitive content') 178 + : (l10n?.labelModerationNote ?? 'Moderation note'), 179 + description: l10n?.messageModerationGuidanceApplies ?? 'Moderation guidance applies here', 158 180 tone: tone, 159 181 ), 160 182 ); ··· 164 186 bsky_moderation.ModerationUI ui, { 165 187 String fallback = 'Sensitive content', 166 188 ModerationLabelResolver? labelResolver, 189 + AppLocalizations? l10n, 167 190 }) { 168 - final labels = moderationBlurLabels(ui, labelResolver: labelResolver); 191 + final labels = moderationBlurLabels(ui, labelResolver: labelResolver, l10n: l10n); 169 192 if (labels.isEmpty) { 170 193 return fallback; 171 194 }
+57 -30
lib/features/moderation/presentation/screens/labeler_detail_screen.dart
··· 2 2 import 'package:bluesky/app_bsky_labeler_defs.dart'; 3 3 import 'package:flutter/material.dart'; 4 4 import 'package:flutter_bloc/flutter_bloc.dart'; 5 + import 'package:lazurite/core/l10n/l10n.dart'; 5 6 import 'package:lazurite/core/theme/theme_extensions.dart'; 6 7 import 'package:lazurite/features/moderation/data/moderation_service.dart'; 7 8 import 'package:lazurite/features/moderation/presentation/moderation_ui_helpers.dart'; ··· 39 40 await _service.ensureInitialized(); 40 41 final details = await _service.getLabelerDetails(widget.did); 41 42 if (details == null) { 42 - throw Exception('Labeler not found.'); 43 + throw const _LabelerNotFoundException(); 43 44 } 44 45 45 46 final currentLabelers = _service.currentPrefs?.labelers.map((labeler) => labeler.did).toSet() ?? const <String>{}; ··· 63 64 _reload(); 64 65 } catch (error) { 65 66 if (mounted) { 66 - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('$error'))); 67 + ScaffoldMessenger.of( 68 + context, 69 + ).showSnackBar(SnackBar(content: Text(context.l10n.errorFailedToUpdateLabelerSubscription(error)))); 67 70 } 68 71 } finally { 69 72 if (mounted) { ··· 78 81 _reload(); 79 82 } catch (error) { 80 83 if (mounted) { 81 - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to update preference: $error'))); 84 + ScaffoldMessenger.of( 85 + context, 86 + ).showSnackBar(SnackBar(content: Text(context.l10n.errorFailedToUpdateLabelPreference(error)))); 82 87 } 83 88 } 84 89 } 85 90 86 91 @override 87 92 Widget build(BuildContext context) { 93 + final l10n = context.l10n; 94 + 88 95 return Scaffold( 89 - appBar: AppBar(title: const Text('Labeler')), 96 + appBar: AppBar(title: Text(l10n.labelLabeler)), 90 97 body: FutureBuilder<_LabelerDetailData>( 91 98 future: _loadFuture, 92 99 builder: (context, snapshot) { ··· 101 108 child: Column( 102 109 mainAxisSize: MainAxisSize.min, 103 110 children: [ 104 - Text('Unable to load labeler', style: context.textTheme.titleMedium), 111 + Text(l10n.errorUnableToLoadLabeler, style: context.textTheme.titleMedium), 105 112 const SizedBox(height: 8), 106 - Text('${snapshot.error}', textAlign: TextAlign.center), 113 + Text(_errorMessageFor(context, snapshot.error), textAlign: TextAlign.center), 107 114 const SizedBox(height: 16), 108 - FilledButton(onPressed: _reload, child: const Text('Retry')), 115 + FilledButton(onPressed: _reload, child: Text(l10n.buttonRetry)), 109 116 ], 110 117 ), 111 118 ), ··· 171 178 spacing: 8, 172 179 runSpacing: 8, 173 180 children: [ 174 - _PolicyChip(label: '${definitions.length} custom labels'), 175 - _PolicyChip(label: '${labeler.policies.labelValues.length} published values'), 176 - if (isOfficial) const _PolicyChip(label: 'Built-in moderation'), 181 + _PolicyChip(label: l10n.formatCustomLabelCount(definitions.length)), 182 + _PolicyChip(label: l10n.formatPublishedValueCount(labeler.policies.labelValues.length)), 183 + if (isOfficial) _PolicyChip(label: l10n.labelBuiltInModeration), 177 184 ], 178 185 ), 179 186 const SizedBox(height: 16), 180 187 SwitchListTile.adaptive( 181 188 value: data.isSubscribed || isOfficial, 182 189 onChanged: isOfficial || _isUpdatingSubscription ? null : _toggleSubscription, 183 - title: Text(isOfficial ? 'Built-in moderation' : 'Subscribed'), 190 + title: Text(isOfficial ? l10n.labelBuiltInModeration : l10n.labelSubscribed), 184 191 subtitle: Text( 185 - isOfficial 186 - ? 'This labeler is always active.' 187 - : 'Subscribed labelers are added to your moderation headers and preferences.', 192 + isOfficial ? l10n.messageBuiltInLabelerAlwaysActive : l10n.messageSubscribedLabelersHeaders, 188 193 ), 189 194 contentPadding: EdgeInsets.zero, 190 195 ), ··· 193 198 ), 194 199 const SizedBox(height: 24), 195 200 Text( 196 - 'Published policies'.toUpperCase(), 201 + l10n.labelPublishedPolicies.toUpperCase(), 197 202 style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700, letterSpacing: 0.8), 198 203 ), 199 204 const SizedBox(height: 8), ··· 212 217 ), 213 218 const SizedBox(height: 24), 214 219 Text( 215 - 'Label preferences'.toUpperCase(), 220 + l10n.labelLabelPreferences.toUpperCase(), 216 221 style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700, letterSpacing: 0.8), 217 222 ), 218 223 const SizedBox(height: 8), 219 224 if (definitions.isEmpty) 220 - const _PreferenceCard( 225 + _PreferenceCard( 221 226 child: ListTile( 222 - title: Text('No custom label definitions'), 223 - subtitle: Text('This labeler publishes values, but not localized custom definitions.'), 227 + title: Text(l10n.labelNoCustomLabelDefinitions), 228 + subtitle: Text(l10n.messageNoCustomLabelDefinitions), 224 229 ), 225 230 ) 226 231 else ··· 240 245 formatLocalizedLabelDescription( 241 246 definition.locales, 242 247 locale, 243 - fallback: 'No description available for this label.', 248 + fallback: l10n.messageNoLabelDescriptionAvailable, 244 249 ), 245 250 style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceVariant), 246 251 ), ··· 249 254 spacing: 8, 250 255 runSpacing: 8, 251 256 children: [ 252 - _PolicyChip(label: 'ID ${definition.identifier}'), 253 - _PolicyChip(label: 'Blur ${definition.blurs.toJson()}'), 254 - _PolicyChip(label: 'Severity ${definition.severity.toJson()}'), 257 + _PolicyChip(label: l10n.formatPolicyId(definition.identifier)), 258 + _PolicyChip(label: l10n.formatPolicyBlur(definition.blurs.toJson())), 259 + _PolicyChip(label: l10n.formatPolicySeverity(definition.severity.toJson())), 255 260 _PolicyChip( 256 - label: 'Default ${visibilityFromDefaultSetting(definition.defaultSetting).name}', 261 + label: l10n.formatPolicyDefault( 262 + visibilityFromDefaultSetting(definition.defaultSetting).name, 263 + ), 257 264 ), 258 - if (definition.adultOnly ?? false) const _PolicyChip(label: '18+'), 265 + if (definition.adultOnly ?? false) _PolicyChip(label: l10n.labelAdultOnlyShort), 259 266 ], 260 267 ), 261 268 const SizedBox(height: 16), 262 269 SegmentedButton<KnownContentLabelPrefVisibility>( 263 - segments: const [ 264 - ButtonSegment(value: KnownContentLabelPrefVisibility.ignore, label: Text('Ignore')), 265 - ButtonSegment(value: KnownContentLabelPrefVisibility.warn, label: Text('Warn')), 266 - ButtonSegment(value: KnownContentLabelPrefVisibility.hide, label: Text('Hide')), 270 + segments: [ 271 + ButtonSegment( 272 + value: KnownContentLabelPrefVisibility.ignore, 273 + label: Text(l10n.labelContentPreferenceIgnore), 274 + ), 275 + ButtonSegment( 276 + value: KnownContentLabelPrefVisibility.warn, 277 + label: Text(l10n.labelContentPreferenceWarn), 278 + ), 279 + ButtonSegment( 280 + value: KnownContentLabelPrefVisibility.hide, 281 + label: Text(l10n.labelContentPreferenceHide), 282 + ), 267 283 ], 268 284 selected: { 269 285 resolveLabelPreference( ··· 281 297 if ((definition.adultOnly ?? false) && !data.adultContentEnabled) ...[ 282 298 const SizedBox(height: 10), 283 299 Text( 284 - 'Enable adult content to change this 18+ label.', 300 + l10n.messageEnableAdultContentForLabel, 285 301 style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 286 302 ), 287 303 ], ··· 311 327 final bool adultContentEnabled; 312 328 final List<UPreferences> currentPreferences; 313 329 final bool isSubscribed; 330 + } 331 + 332 + class _LabelerNotFoundException implements Exception { 333 + const _LabelerNotFoundException(); 334 + } 335 + 336 + String _errorMessageFor(BuildContext context, Object? error) { 337 + if (error is _LabelerNotFoundException) { 338 + return context.l10n.errorLabelerNotFound; 339 + } 340 + return '$error'; 314 341 } 315 342 316 343 class _PolicyChip extends StatelessWidget {
+43 -40
lib/features/moderation/presentation/screens/moderation_settings_screen.dart
··· 3 3 import 'package:flutter/material.dart'; 4 4 import 'package:flutter_bloc/flutter_bloc.dart'; 5 5 import 'package:go_router/go_router.dart'; 6 + import 'package:lazurite/core/l10n/l10n.dart'; 7 + import 'package:lazurite/core/theme/theme_extensions.dart'; 6 8 import 'package:lazurite/features/moderation/data/moderation_service.dart'; 7 9 import 'package:lazurite/features/moderation/presentation/moderation_ui_helpers.dart'; 8 10 import 'package:lazurite/features/moderation/presentation/widgets/moderated_avatar.dart'; 9 11 import 'package:lazurite/shared/presentation/helpers/snackbar_helper.dart'; 10 12 import 'package:lazurite/shared/presentation/widgets/confirmation_dialog.dart'; 11 13 import 'package:lazurite/shared/utils/format_utils.dart'; 12 - import 'package:lazurite/core/theme/theme_extensions.dart'; 13 14 14 15 class ModerationSettingsScreen extends StatefulWidget { 15 16 const ModerationSettingsScreen({super.key}); ··· 64 65 _reload(); 65 66 } catch (error) { 66 67 if (mounted) { 67 - showAppSnackBar(context, 'Failed to update adult content: $error', isError: true); 68 + showAppSnackBar(context, context.l10n.errorFailedToUpdateAdultContent(error), isError: true); 68 69 } 69 70 } finally { 70 71 if (mounted) { ··· 79 80 _reload(); 80 81 } catch (error) { 81 82 if (mounted) { 82 - showAppSnackBar(context, 'Failed to unsubscribe: $error', isError: true); 83 + showAppSnackBar(context, context.l10n.errorFailedToUnsubscribeLabeler(error), isError: true); 83 84 } 84 85 } 85 86 } 86 87 87 88 Future<void> _showAddLabelerDialog() async { 88 89 final controller = TextEditingController(); 90 + final rootContext = context; 89 91 String? errorText; 90 92 91 93 await showDialog<void>( ··· 94 96 var isSubmitting = false; 95 97 96 98 return StatefulBuilder( 97 - builder: (context, setDialogState) { 99 + builder: (builderContext, setDialogState) { 100 + final l10n = builderContext.l10n; 101 + 98 102 Future<void> submit() async { 99 103 final did = controller.text.trim(); 100 104 if (did.isEmpty) { 101 - setDialogState(() => errorText = 'Enter a labeler DID.'); 105 + setDialogState(() => errorText = l10n.errorLabelerDidRequired); 102 106 return; 103 107 } 104 108 ··· 110 114 try { 111 115 final details = await _service.getLabelerDetails(did); 112 116 if (details == null) { 113 - setDialogState(() => errorText = 'No labeler found for that DID.'); 117 + setDialogState(() => errorText = l10n.errorNoLabelerFoundForDid); 114 118 return; 115 119 } 116 120 ··· 120 124 Navigator.of(dialogContext).pop(); 121 125 _reload(); 122 126 123 - if (context.mounted) { 127 + if (rootContext.mounted) { 124 128 final name = details.creator.displayName ?? details.creator.handle; 125 - showAppSnackBar(context, 'Subscribed to $name'); 129 + showAppSnackBar(rootContext, rootContext.l10n.formatSubscribedToLabeler(name)); 126 130 } 127 131 } 128 132 } catch (error) { ··· 133 137 } 134 138 135 139 return ConfirmationDialog( 136 - title: const Text('Add labeler'), 140 + title: Text(l10n.dialogAddLabelerTitle), 137 141 content: SizedBox( 138 142 width: 420, 139 143 child: Column( ··· 143 147 controller: controller, 144 148 autofocus: true, 145 149 decoration: InputDecoration( 146 - labelText: 'Labeler DID', 147 - hintText: 'did:plc:examplelabeler', 150 + labelText: l10n.labelLabelerDid, 151 + hintText: l10n.placeholderLabelerDid, 148 152 errorText: errorText, 149 153 border: const OutlineInputBorder(), 150 154 ), 151 155 onSubmitted: (_) => isSubmitting ? null : submit(), 152 156 ), 153 157 const SizedBox(height: 12), 154 - Text( 155 - 'Paste a labeler DID to review and subscribe to its labels.', 156 - style: context.textTheme.bodySmall, 157 - ), 158 + Text(l10n.messageAddLabelerDidHelper, style: builderContext.textTheme.bodySmall), 158 159 ], 159 160 ), 160 161 ), 161 - confirmLabel: isSubmitting ? 'Adding...' : 'Add', 162 + confirmLabel: isSubmitting ? l10n.buttonAdding : l10n.buttonAdd, 162 163 confirmEnabled: !isSubmitting, 163 164 onCancel: isSubmitting ? null : () => Navigator.of(dialogContext).pop(), 164 165 onConfirm: submit, ··· 171 172 172 173 @override 173 174 Widget build(BuildContext context) { 175 + final l10n = context.l10n; 176 + 174 177 return Scaffold( 175 178 appBar: AppBar( 176 - title: const Text('Moderation'), 177 - actions: [IconButton(tooltip: 'Refresh', onPressed: _reload, icon: const Icon(Icons.refresh))], 179 + title: Text(l10n.labelModeration), 180 + actions: [IconButton(tooltip: l10n.labelRefresh, onPressed: _reload, icon: const Icon(Icons.refresh))], 178 181 ), 179 182 body: FutureBuilder<_ModerationSettingsData>( 180 183 future: _loadFuture, ··· 190 193 child: Column( 191 194 mainAxisSize: MainAxisSize.min, 192 195 children: [ 193 - Text('Failed to load moderation settings', style: context.textTheme.titleMedium), 196 + Text(l10n.errorFailedToLoadModerationSettings, style: context.textTheme.titleMedium), 194 197 const SizedBox(height: 8), 195 198 Text('${snapshot.error}', textAlign: TextAlign.center), 196 199 const SizedBox(height: 16), 197 - FilledButton(onPressed: _reload, child: const Text('Retry')), 200 + FilledButton(onPressed: _reload, child: Text(l10n.buttonRetry)), 198 201 ], 199 202 ), 200 203 ), ··· 209 212 child: ListView( 210 213 padding: const EdgeInsets.all(16), 211 214 children: [ 212 - const _SettingsHero( 213 - title: 'Labelers and content moderation', 214 - subtitle: 215 - 'Manage adult-content visibility, subscribed labelers, and the rules each labeler applies to posts and profiles.', 215 + _SettingsHero( 216 + title: l10n.labelLabelersAndContentModeration, 217 + subtitle: l10n.messageModerationSettingsHeroSubtitle, 216 218 ), 217 219 const SizedBox(height: 16), 218 220 _SettingsCard( 219 221 child: SwitchListTile.adaptive( 220 222 value: data.adultContentEnabled, 221 223 onChanged: _isUpdatingAdultContent ? null : _toggleAdultContent, 222 - title: const Text('Adult content'), 223 - subtitle: const Text('Required before 18+ label preferences can be changed.'), 224 + title: Text(l10n.labelAdultContentSetting), 225 + subtitle: Text(l10n.messageAdultContentRequiredForLabels), 224 226 contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), 225 227 ), 226 228 ), 227 229 const SizedBox(height: 24), 228 230 _SectionHeader( 229 - title: 'Built-in labeler', 231 + title: l10n.labelBuiltInLabeler, 230 232 trailing: Text( 231 - 'Always on', 233 + l10n.labelAlwaysOn, 232 234 style: context.textTheme.labelMedium?.copyWith(color: context.colorScheme.primary), 233 235 ), 234 236 ), ··· 243 245 ), 244 246 ) 245 247 else 246 - const _SettingsCard( 248 + _SettingsCard( 247 249 child: ListTile( 248 - title: Text('Bluesky moderation'), 249 - subtitle: Text('The built-in labeler is active even if its details cannot be loaded right now.'), 250 + title: Text(l10n.labelBlueskyModeration), 251 + subtitle: Text(l10n.messageBuiltInLabelerActiveWhenUnavailable), 250 252 ), 251 253 ), 252 254 const SizedBox(height: 24), 253 255 _SectionHeader( 254 - title: 'Custom labelers', 256 + title: l10n.labelCustomLabelers, 255 257 trailing: FilledButton.tonalIcon( 256 258 onPressed: _showAddLabelerDialog, 257 259 icon: const Icon(Icons.add), 258 - label: Text('Add (${labelers.length}/20)'), 260 + label: Text(l10n.formatAddLabelerLimit(labelers.length, 20)), 259 261 ), 260 262 ), 261 263 const SizedBox(height: 8), 262 264 if (labelers.isEmpty) 263 - const _SettingsCard( 265 + _SettingsCard( 264 266 child: ListTile( 265 - title: Text('No custom labelers'), 266 - subtitle: Text('Add a labeler DID to subscribe and configure its custom labels.'), 267 + title: Text(l10n.labelNoCustomLabelers), 268 + subtitle: Text(l10n.messageNoCustomLabelers), 267 269 ), 268 270 ) 269 271 else ··· 390 392 391 393 @override 392 394 Widget build(BuildContext context) { 395 + final l10n = context.l10n; 393 396 final creator = labeler.creator; 394 397 final definitions = labeler.policies.labelValueDefinitions ?? const []; 395 398 ··· 431 434 borderRadius: BorderRadius.circular(999), 432 435 ), 433 436 child: Text( 434 - 'Built-in', 437 + l10n.labelBuiltIn, 435 438 style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700), 436 439 ), 437 440 ), ··· 456 459 spacing: 8, 457 460 runSpacing: 8, 458 461 children: [ 459 - _MetaChip(label: '${definitions.length} definitions'), 460 - _MetaChip(label: '${labeler.policies.labelValues.length} published values'), 462 + _MetaChip(label: l10n.formatDefinitionCount(definitions.length)), 463 + _MetaChip(label: l10n.formatPublishedValueCount(labeler.policies.labelValues.length)), 461 464 ], 462 465 ), 463 466 ], ··· 468 471 children: [ 469 472 Icon(isSubscribed ? Icons.chevron_right : Icons.add_circle_outline), 470 473 if (!isOfficial && onUnsubscribe != null) 471 - TextButton(onPressed: onUnsubscribe, child: const Text('Unsubscribe')), 474 + TextButton(onPressed: onUnsubscribe, child: Text(l10n.buttonUnsubscribe)), 472 475 ], 473 476 ), 474 477 ],
+9 -4
lib/features/moderation/presentation/widgets/moderated_blur_overlay.dart
··· 1 1 import 'dart:ui'; 2 - import 'package:lazurite/core/l10n/l10n.dart'; 3 - import 'package:lazurite/core/theme/theme_extensions.dart'; 4 2 5 3 import 'package:bluesky/moderation.dart' as bsky_moderation; 6 4 import 'package:flutter/material.dart'; 5 + import 'package:lazurite/core/l10n/l10n.dart'; 6 + import 'package:lazurite/core/theme/theme_extensions.dart'; 7 7 import 'package:lazurite/features/moderation/presentation/moderation_ui_helpers.dart'; 8 8 9 + const _defaultModerationFallbackLabel = 'Sensitive content'; 10 + 9 11 class ModeratedBlurOverlay extends StatefulWidget { 10 12 const ModeratedBlurOverlay({ 11 13 super.key, 12 14 required this.ui, 13 15 required this.child, 14 16 this.borderRadius, 15 - this.fallbackLabel = 'Sensitive content', 17 + this.fallbackLabel = _defaultModerationFallbackLabel, 16 18 this.fillWidth = true, 17 19 this.labelResolver, 18 20 }); ··· 81 83 Text( 82 84 moderationOverlayTitle( 83 85 widget.ui, 84 - fallback: widget.fallbackLabel, 86 + fallback: widget.fallbackLabel == _defaultModerationFallbackLabel 87 + ? l10n.labelSensitiveContent 88 + : widget.fallbackLabel, 85 89 labelResolver: effectiveResolver, 90 + l10n: l10n, 86 91 ), 87 92 textAlign: TextAlign.center, 88 93 style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700),
+4 -2
lib/features/moderation/presentation/widgets/moderation_badge_row.dart
··· 1 1 import 'package:bluesky/moderation.dart' as bsky_moderation; 2 2 import 'package:flutter/material.dart'; 3 + import 'package:lazurite/core/l10n/l10n.dart'; 4 + import 'package:lazurite/core/theme/theme_extensions.dart'; 3 5 import 'package:lazurite/features/moderation/presentation/moderation_ui_helpers.dart'; 4 - import 'package:lazurite/core/theme/theme_extensions.dart'; 5 6 6 7 class ModerationBadgeRow extends StatelessWidget { 7 8 const ModerationBadgeRow({super.key, required this.ui, this.padding = EdgeInsets.zero, this.labelResolver}); ··· 12 13 13 14 @override 14 15 Widget build(BuildContext context) { 16 + final l10n = context.l10n; 15 17 final moderationService = maybeModerationService(context); 16 18 final locale = Localizations.localeOf(context); 17 19 final effectiveResolver = ··· 24 26 preferredLanguages: [locale.toLanguageTag(), locale.languageCode], 25 27 )); 26 28 27 - final badges = moderationBadgesForUi(ui, labelResolver: effectiveResolver); 29 + final badges = moderationBadgesForUi(ui, labelResolver: effectiveResolver, l10n: l10n); 28 30 if (badges.isEmpty) { 29 31 return const SizedBox.shrink(); 30 32 }
+11 -7
lib/features/notifications/domain/notification_local_mappers.dart
··· 1 1 import 'dart:convert'; 2 + import 'dart:ui'; 2 3 3 4 import 'package:bluesky/app_bsky_notification_listnotifications.dart'; 4 5 import 'package:crypto/crypto.dart'; 6 + import 'package:lazurite/core/l10n/app_localizations.dart'; 5 7 import 'package:lazurite/features/notifications/domain/notification_local_models.dart'; 6 8 import 'package:lazurite/features/notifications/domain/notification_reason_utils.dart'; 7 9 ··· 39 41 } 40 42 41 43 class NotificationLocalMapper { 44 + static final AppLocalizations _fallbackL10n = lookupAppLocalizations(const Locale('en')); 45 + 42 46 static LocalNotificationRequest? requestFromNotification(Notification notification) { 43 47 if (notification.isRead) { 44 48 return null; ··· 52 56 return LocalNotificationRequest( 53 57 notificationId: _stableNotificationId(notification.uri.toString()), 54 58 title: _titleForNotification(notification), 55 - body: NotificationReasonUtils.localNotificationBodyForReason(notification.reason), 59 + body: NotificationReasonUtils.localNotificationBodyForReason(notification.reason, l10n: _fallbackL10n), 56 60 reasonFamily: NotificationReasonUtils.reasonFamilyForReason(notification.reason), 57 61 deepLink: deepLink, 58 62 ); ··· 64 68 return displayName; 65 69 } 66 70 final handle = notification.author.handle.trim(); 67 - return handle.isEmpty ? 'New notification' : handle; 71 + return handle.isEmpty ? _fallbackL10n.messageNewNotification : handle; 68 72 } 69 73 70 74 static int _stableNotificationId(String value) { ··· 93 97 String get androidChannelName { 94 98 switch (this) { 95 99 case NotificationReasonFamily.mentions: 96 - return 'Mentions'; 100 + return NotificationLocalMapper._fallbackL10n.labelMentions; 97 101 case NotificationReasonFamily.replies: 98 - return 'Replies'; 102 + return NotificationLocalMapper._fallbackL10n.labelReplies; 99 103 case NotificationReasonFamily.follows: 100 - return 'Follows'; 104 + return NotificationLocalMapper._fallbackL10n.labelFollows; 101 105 case NotificationReasonFamily.likes: 102 - return 'Likes'; 106 + return NotificationLocalMapper._fallbackL10n.labelLikes; 103 107 case NotificationReasonFamily.misc: 104 - return 'Other'; 108 + return NotificationLocalMapper._fallbackL10n.labelOther; 105 109 } 106 110 } 107 111
+26 -19
lib/features/notifications/domain/notification_reason_utils.dart
··· 1 1 import 'package:atproto_core/atproto_core.dart'; 2 2 import 'package:bluesky/app_bsky_notification_listnotifications.dart' as bsky; 3 + import 'dart:ui'; 4 + 5 + import 'package:lazurite/core/l10n/app_localizations.dart'; 3 6 import 'package:lazurite/features/notifications/domain/notification_local_models.dart'; 4 7 5 8 abstract final class NotificationReasonUtils { ··· 35 38 } 36 39 } 37 40 38 - static String summaryTextForReason(bsky.NotificationReason reason) { 41 + static final AppLocalizations _fallbackL10n = lookupAppLocalizations(const Locale('en')); 42 + 43 + static String summaryTextForReason(bsky.NotificationReason reason, {AppLocalizations? l10n}) { 44 + final strings = l10n ?? _fallbackL10n; 39 45 if (!reason.isKnownValue) { 40 - return 'interacted with you'; 46 + return strings.messageNotificationInteracted; 41 47 } 42 48 43 49 switch (reason.knownValue) { 44 50 case bsky.KnownNotificationReason.like: 45 - return 'liked your post'; 51 + return strings.messageNotificationLike; 46 52 case bsky.KnownNotificationReason.repost: 47 - return 'reposted your post'; 53 + return strings.messageNotificationRepost; 48 54 case bsky.KnownNotificationReason.likeViaRepost: 49 - return 'liked your repost'; 55 + return strings.messageNotificationLikeViaRepost; 50 56 case bsky.KnownNotificationReason.repostViaRepost: 51 - return 'reposted your repost'; 57 + return strings.messageNotificationRepostViaRepost; 52 58 case bsky.KnownNotificationReason.follow: 53 - return 'followed you'; 59 + return strings.messageNotificationFollow; 54 60 case bsky.KnownNotificationReason.mention: 55 - return 'mentioned you'; 61 + return strings.messageNotificationMention; 56 62 case bsky.KnownNotificationReason.reply: 57 - return 'replied to your post'; 63 + return strings.messageNotificationReply; 58 64 case bsky.KnownNotificationReason.quote: 59 - return 'quoted your post'; 65 + return strings.messageNotificationQuote; 60 66 case bsky.KnownNotificationReason.starterpackJoined: 61 - return 'joined via your starter pack'; 67 + return strings.messageNotificationStarterPackJoined; 62 68 case bsky.KnownNotificationReason.verified: 63 - return 'verified your account'; 69 + return strings.messageNotificationVerified; 64 70 case bsky.KnownNotificationReason.unverified: 65 - return 'removed your verification'; 71 + return strings.messageNotificationUnverified; 66 72 case bsky.KnownNotificationReason.subscribedPost: 67 - return 'posted a new update'; 73 + return strings.messageNotificationSubscribedPost; 68 74 case bsky.KnownNotificationReason.contactMatch: 69 - return 'joined from your contacts'; 75 + return strings.messageNotificationContactMatch; 70 76 default: 71 - return 'interacted with you'; 77 + return strings.messageNotificationInteracted; 72 78 } 73 79 } 74 80 75 - static String localNotificationBodyForReason(bsky.NotificationReason reason) { 76 - final summary = summaryTextForReason(reason); 77 - return summary == 'interacted with you' ? 'sent a notification' : summary; 81 + static String localNotificationBodyForReason(bsky.NotificationReason reason, {AppLocalizations? l10n}) { 82 + final strings = l10n ?? _fallbackL10n; 83 + final summary = summaryTextForReason(reason, l10n: strings); 84 + return summary == strings.messageNotificationInteracted ? strings.messageLocalNotificationFallbackBody : summary; 78 85 } 79 86 80 87 static NotificationReasonFamily reasonFamilyForReason(bsky.NotificationReason reason) {
+3 -2
lib/features/notifications/presentation/notifications_screen.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:flutter_bloc/flutter_bloc.dart'; 3 + import 'package:lazurite/core/l10n/l10n.dart'; 3 4 import 'package:lazurite/core/widgets/lazurite_app_bar.dart'; 4 5 import 'package:lazurite/features/notifications/bloc/notification_bloc.dart'; 5 6 import 'package:lazurite/features/notifications/cubit/unread_count_cubit.dart'; ··· 12 13 Widget build(BuildContext context) { 13 14 return Scaffold( 14 15 appBar: LazuriteAppBar( 15 - sectionLabel: 'Alerts', 16 - actions: [TextButton(onPressed: () => _markAllRead(context), child: const Text('Mark All Read'))], 16 + sectionLabel: context.l10n.labelAlertsTitle, 17 + actions: [TextButton(onPressed: () => _markAllRead(context), child: Text(context.l10n.buttonMarkAllRead))], 17 18 ), 18 19 body: const NotificationsPane(), 19 20 );
+15 -14
lib/features/notifications/presentation/widgets/grouped_notification_list_item.dart
··· 3 3 import 'package:bluesky/moderation.dart' as bsky_moderation; 4 4 import 'package:flutter/material.dart' hide Notification; 5 5 import 'package:go_router/go_router.dart'; 6 + import 'package:lazurite/core/l10n/l10n.dart'; 7 + import 'package:lazurite/core/theme/theme_extensions.dart'; 6 8 import 'package:lazurite/features/moderation/presentation/moderation_ui_helpers.dart'; 7 9 import 'package:lazurite/features/moderation/presentation/widgets/moderated_blur_overlay.dart'; 8 10 import 'package:lazurite/features/notifications/domain/notification_deep_link_navigator.dart'; ··· 10 12 import 'package:lazurite/shared/presentation/helpers/notification_icon_mapper.dart'; 11 13 import 'package:lazurite/shared/presentation/widgets/profile_avatar.dart'; 12 14 import 'package:lazurite/shared/utils/format_utils.dart'; 13 - import 'package:lazurite/core/theme/theme_extensions.dart'; 14 15 15 16 class NotificationGroup { 16 17 const NotificationGroup({required this.notifications}); ··· 66 67 children: [ 67 68 _buildActorRow(context), 68 69 const SizedBox(height: 4), 69 - _buildSummary(theme), 70 + _buildSummary(context, theme), 70 71 const SizedBox(height: 2), 71 - _buildTime(theme), 72 + _buildTime(context, theme), 72 73 if (_shouldShowPreview(latest)) ...[ 73 74 const SizedBox(height: 8), 74 75 _buildPreview(context, theme, latest), ··· 144 145 ); 145 146 } 146 147 147 - Widget _buildSummary(ThemeData theme) { 148 + Widget _buildSummary(BuildContext context, ThemeData theme) { 148 149 return RichText( 149 150 text: TextSpan( 150 151 children: [ 151 152 TextSpan( 152 - text: _actorSummary(), 153 + text: _actorSummary(context), 153 154 style: theme.textTheme.bodyMedium?.copyWith( 154 155 fontWeight: FontWeight.w600, 155 156 color: theme.colorScheme.onSurface, 156 157 ), 157 158 ), 158 159 TextSpan( 159 - text: ' ${_getReasonText(group.latest)}', 160 + text: ' ${_getReasonText(context, group.latest)}', 160 161 style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant), 161 162 ), 162 163 ], ··· 164 165 ); 165 166 } 166 167 167 - String _actorSummary() { 168 + String _actorSummary(BuildContext context) { 168 169 final names = group.authors.map((author) => author.displayName ?? author.handle).toList(); 169 170 if (names.isEmpty) { 170 - return 'Someone'; 171 + return context.l10n.labelSomeone; 171 172 } 172 173 if (names.length == 1) { 173 174 return names.first; 174 175 } 175 176 if (names.length == 2) { 176 - return '${names[0]} and ${names[1]}'; 177 + return context.l10n.formatActorListTwo(names[0], names[1]); 177 178 } 178 - return '${names[0]}, ${names[1]}, and ${names.length - 2} others'; 179 + return context.l10n.formatActorListWithOthers(names[0], names[1], names.length - 2); 179 180 } 180 181 181 - Widget _buildTime(ThemeData theme) { 182 + Widget _buildTime(BuildContext context, ThemeData theme) { 182 183 return Text( 183 - formatRelativeTime(group.latest.indexedAt, nowLabel: 'Just now', includeAgo: true), 184 + formatRelativeTime(group.latest.indexedAt, nowLabel: context.l10n.commonJustNow, includeAgo: true), 184 185 style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant), 185 186 ); 186 187 } 187 188 188 - String _getReasonText(bsky.Notification notification) { 189 - return NotificationReasonUtils.summaryTextForReason(notification.reason); 189 + String _getReasonText(BuildContext context, bsky.Notification notification) { 190 + return NotificationReasonUtils.summaryTextForReason(notification.reason, l10n: context.l10n); 190 191 } 191 192 192 193 bool _shouldShowPreview(bsky.Notification notification) {
+9 -8
lib/features/notifications/presentation/widgets/notification_list_item.dart
··· 2 2 import 'package:bluesky/moderation.dart' as bsky_moderation; 3 3 import 'package:flutter/material.dart' hide Notification; 4 4 import 'package:go_router/go_router.dart'; 5 + import 'package:lazurite/core/l10n/l10n.dart'; 5 6 import 'package:lazurite/features/moderation/presentation/moderation_ui_helpers.dart'; 6 7 import 'package:lazurite/features/moderation/presentation/widgets/moderated_blur_overlay.dart'; 7 8 import 'package:lazurite/features/moderation/presentation/widgets/moderation_badge_row.dart'; ··· 45 46 children: [ 46 47 _buildActorRow(context), 47 48 const SizedBox(height: 4), 48 - _buildSummary(theme), 49 + _buildSummary(context, theme), 49 50 const SizedBox(height: 2), 50 - _buildTime(theme), 51 + _buildTime(context, theme), 51 52 if (notificationUi.alert || notificationUi.inform) ...[ 52 53 const SizedBox(height: 8), 53 54 ModerationBadgeRow(ui: notificationUi), ··· 95 96 ); 96 97 } 97 98 98 - Widget _buildSummary(ThemeData theme) { 99 + Widget _buildSummary(BuildContext context, ThemeData theme) { 99 100 final author = notification.author; 100 101 final displayName = author.displayName ?? author.handle; 101 - final reasonText = _getReasonText(); 102 + final reasonText = _getReasonText(context); 102 103 103 104 return RichText( 104 105 text: TextSpan( ··· 119 120 ); 120 121 } 121 122 122 - String _getReasonText() { 123 - return NotificationReasonUtils.summaryTextForReason(notification.reason); 123 + String _getReasonText(BuildContext context) { 124 + return NotificationReasonUtils.summaryTextForReason(notification.reason, l10n: context.l10n); 124 125 } 125 126 126 - Widget _buildTime(ThemeData theme) { 127 + Widget _buildTime(BuildContext context, ThemeData theme) { 127 128 return Text( 128 - formatRelativeTime(notification.indexedAt, nowLabel: 'Just now', includeAgo: true), 129 + formatRelativeTime(notification.indexedAt, nowLabel: context.l10n.commonJustNow, includeAgo: true), 129 130 style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant), 130 131 ); 131 132 }
+22 -30
lib/features/notifications/presentation/widgets/notifications_pane.dart
··· 3 3 4 4 import 'package:flutter/material.dart'; 5 5 import 'package:flutter_bloc/flutter_bloc.dart'; 6 + import 'package:intl/intl.dart'; 7 + import 'package:lazurite/core/l10n/l10n.dart'; 8 + import 'package:lazurite/core/theme/theme_extensions.dart'; 6 9 import 'package:lazurite/features/connectivity/cubit/connectivity_cubit.dart'; 7 10 import 'package:lazurite/features/notifications/bloc/notification_bloc.dart'; 8 11 import 'package:lazurite/features/notifications/cubit/unread_count_cubit.dart'; ··· 13 16 import 'package:lazurite/shared/presentation/widgets/error_state.dart'; 14 17 import 'package:lazurite/shared/presentation/widgets/loading_state.dart'; 15 18 import 'package:lazurite/shared/presentation/widgets/staggered_entrance.dart'; 16 - import 'package:lazurite/core/theme/theme_extensions.dart'; 17 19 18 20 class NotificationsPane extends StatefulWidget { 19 21 const NotificationsPane({super.key}); ··· 89 91 return const _OfflineNotificationsState(); 90 92 } 91 93 return ErrorState( 92 - title: 'Failed to load notifications', 93 - message: state.errorMessage ?? 'Unknown error', 94 + title: context.l10n.errorFailedToLoadNotifications, 95 + message: state.errorMessage ?? context.l10n.errorUnknown, 94 96 onRetry: () => context.read<NotificationBloc>().add(const NotificationsRequested()), 95 97 ); 96 98 } ··· 99 101 if (isOffline) { 100 102 return const _OfflineNotificationsState(); 101 103 } 102 - return const EmptyState(message: 'No notifications yet', icon: Icons.notifications_none_outlined); 104 + return EmptyState(message: context.l10n.messageNoNotificationsYet, icon: Icons.notifications_none_outlined); 103 105 } 104 106 105 107 final groupedNotifications = _groupNotificationsByDay(state.notifications); ··· 110 112 controller: _scrollController, 111 113 itemCount: _calculateItemCount(groupedNotifications, state), 112 114 itemBuilder: (context, index) { 113 - final item = _getItemAtIndex(groupedNotifications, index); 115 + final item = _getItemAtIndex(context, groupedNotifications, index); 114 116 115 117 if (item is String) { 116 118 return _DayHeader(title: item); ··· 185 187 return count; 186 188 } 187 189 188 - dynamic _getItemAtIndex(Map<DateTime, List<NotificationGroup>> grouped, int index) { 190 + dynamic _getItemAtIndex(BuildContext context, Map<DateTime, List<NotificationGroup>> grouped, int index) { 189 191 int currentIndex = 0; 190 192 191 193 for (final entry in grouped.entries) { 192 194 if (currentIndex == index) { 193 - return _formatDayHeader(entry.key); 195 + return _formatDayHeader(context, entry.key); 194 196 } 195 197 currentIndex++; 196 198 ··· 205 207 return null; 206 208 } 207 209 208 - String _formatDayHeader(DateTime date) { 210 + String _formatDayHeader(BuildContext context, DateTime date) { 209 211 final now = DateTime.now(); 210 212 final today = DateTime(now.year, now.month, now.day); 211 213 final yesterday = today.subtract(const Duration(days: 1)); 212 214 213 215 if (date == today) { 214 - return 'Today'; 216 + return context.l10n.messageToday; 215 217 } else if (date == yesterday) { 216 - return 'Yesterday'; 218 + return context.l10n.messageYesterday; 217 219 } 218 220 219 - return _formatDate(date); 221 + return _formatDate(context, date); 220 222 } 221 223 222 - String _formatDate(DateTime date) { 223 - const months = [ 224 - 'January', 225 - 'February', 226 - 'March', 227 - 'April', 228 - 'May', 229 - 'June', 230 - 'July', 231 - 'August', 232 - 'September', 233 - 'October', 234 - 'November', 235 - 'December', 236 - ]; 237 - 238 - return '${months[date.month - 1]} ${date.day}'; 224 + String _formatDate(BuildContext context, DateTime date) { 225 + final locale = Localizations.localeOf(context).toLanguageTag(); 226 + return DateFormat.MMMMd(locale).format(date); 239 227 } 240 228 } 241 229 ··· 252 240 children: [ 253 241 Icon(Icons.cloud_off_outlined, size: 48, color: context.colorScheme.outline), 254 242 const SizedBox(height: 12), 255 - Text('No connection', style: context.textTheme.titleMedium), 243 + Text(context.l10n.messageNoConnection, style: context.textTheme.titleMedium), 256 244 const SizedBox(height: 8), 257 - Text('Reconnect to load notifications.', textAlign: TextAlign.center, style: context.textTheme.bodyMedium), 245 + Text( 246 + context.l10n.messageReconnectToLoadNotifications, 247 + textAlign: TextAlign.center, 248 + style: context.textTheme.bodyMedium, 249 + ), 258 250 ], 259 251 ), 260 252 ),
+41 -45
lib/features/profile/presentation/follow_audit_screen.dart
··· 1 1 import 'dart:math' as math; 2 + import 'package:lazurite/core/l10n/l10n.dart'; 2 3 import 'package:lazurite/core/theme/theme_extensions.dart'; 3 4 4 5 import 'package:flutter/material.dart'; ··· 15 16 @override 16 17 Widget build(BuildContext context) { 17 18 return Scaffold( 18 - appBar: AppBar(title: const Text('Audit Followers')), 19 + appBar: AppBar(title: Text(context.l10n.labelAuditFollowers)), 19 20 body: BlocBuilder<FollowAuditCubit, FollowAuditState>( 20 21 builder: (context, state) { 21 22 final visibleEntries = _visibleEntries(state); ··· 43 44 child: Align( 44 45 alignment: Alignment.centerLeft, 45 46 child: Text( 46 - '${state.failedProfiles} profile(s) could not be loaded.', 47 + context.l10n.formatProfilesFailedToLoad(state.failedProfiles), 47 48 key: const Key('follow_audit_failed_warning'), 48 49 style: TextStyle(color: context.colorScheme.tertiary), 49 50 ), 50 51 ), 51 52 ), 52 53 if (state.status == FollowAuditStatus.error) 53 - _ErrorBanner(message: state.errorMessage ?? 'Failed to complete follow audit.'), 54 + _ErrorBanner(message: state.errorMessage ?? context.l10n.errorFollowAuditFailed), 54 55 if (state.status == FollowAuditStatus.complete && state.unfollowedCount > 0) 55 56 Padding( 56 57 padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), 57 58 child: Align( 58 59 alignment: Alignment.centerLeft, 59 60 child: Text( 60 - 'Unfollowed ${state.unfollowedCount} account(s)', 61 + context.l10n.formatUnfollowedAccounts(state.unfollowedCount), 61 62 key: const Key('follow_audit_complete_message'), 62 63 style: context.textTheme.titleMedium, 63 64 ), ··· 147 148 crossAxisAlignment: CrossAxisAlignment.start, 148 149 children: [ 149 150 Text( 150 - 'AUDIT FOLLOWERS', 151 + context.l10n.labelAuditFollowers.toUpperCase(), 151 152 style: context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700, letterSpacing: 1.1), 152 153 ), 153 154 const SizedBox(height: 6), 154 155 Text( 155 - totalFollows > 0 156 - ? '$totalFollows follows scanned for problematic accounts' 157 - : 'Scan your follows for deleted, suspended, blocked, and hidden accounts.', 156 + totalFollows > 0 ? context.l10n.formatFollowsScanned(totalFollows) : context.l10n.messageFollowAuditIntro, 158 157 style: context.textTheme.bodyMedium, 159 158 ), 160 159 ], ··· 179 178 key: const Key('follow_audit_scan_button'), 180 179 onPressed: isBusy ? null : () => context.read<FollowAuditCubit>().audit(), 181 180 icon: const Icon(Icons.manage_search_outlined), 182 - label: const Text('Scan'), 181 + label: Text(context.l10n.buttonScan), 183 182 ); 184 183 } 185 184 ··· 187 186 key: const Key('follow_audit_unfollow_button'), 188 187 onPressed: selectedCount == 0 || isBusy ? null : () => context.read<FollowAuditCubit>().confirmUnfollow(), 189 188 icon: const Icon(Icons.person_remove_outlined), 190 - label: Text('Unfollow Selected ($selectedCount)'), 189 + label: Text(context.l10n.buttonUnfollowSelected(selectedCount)), 191 190 ); 192 191 } 193 192 } ··· 208 207 final shownTotal = totalFollows > 0 ? totalFollows : math.max(progress, 1); 209 208 final value = (progress / shownTotal).clamp(0.0, 1.0); 210 209 final label = status == FollowAuditStatus.fetching 211 - ? 'Fetching follows: $progress/$shownTotal' 212 - : 'Classifying: $progress/$shownTotal'; 210 + ? context.l10n.formatFetchingFollowsProgress(progress, shownTotal) 211 + : context.l10n.formatClassifyingProgress(progress, shownTotal); 213 212 214 213 return Padding( 215 214 padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), ··· 255 254 OutlinedButton( 256 255 key: const Key('follow_audit_retry_button'), 257 256 onPressed: () => context.read<FollowAuditCubit>().audit(), 258 - child: const Text('Retry'), 257 + child: Text(context.l10n.buttonRetry), 259 258 ), 260 259 ], 261 260 ), ··· 305 304 Padding( 306 305 padding: const EdgeInsets.fromLTRB(4, 8, 4, 8), 307 306 child: Text( 308 - 'FILTERS', 307 + context.l10n.labelFilters.toUpperCase(), 309 308 style: context.textTheme.labelMedium?.copyWith(letterSpacing: 1.0, fontWeight: FontWeight.w700), 310 309 ), 311 310 ), ··· 363 362 children: [ 364 363 Expanded( 365 364 child: Text( 366 - _labelForStatus(status).toUpperCase(), 365 + _labelForStatus(context, status).toUpperCase(), 367 366 maxLines: 1, 368 367 overflow: TextOverflow.ellipsis, 369 368 style: context.textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w700), ··· 387 386 visualDensity: VisualDensity.compact, 388 387 icon: Icon(isVisible ? Icons.visibility_outlined : Icons.visibility_off_outlined), 389 388 onPressed: () => context.read<FollowAuditCubit>().toggleVisibility(status), 390 - tooltip: isVisible ? 'Hide ${_labelForStatus(status)}' : 'Show ${_labelForStatus(status)}', 389 + tooltip: isVisible 390 + ? context.l10n.formatHideStatus(_labelForStatus(context, status)) 391 + : context.l10n.formatShowStatus(_labelForStatus(context, status)), 391 392 ), 392 393 Checkbox( 393 394 key: Key('follow_audit_select_all_${status.name}'), ··· 405 406 ), 406 407 Expanded( 407 408 child: Text( 408 - 'Select All', 409 + context.l10n.labelSelectAll, 409 410 maxLines: 1, 410 411 overflow: TextOverflow.ellipsis, 411 412 style: context.textTheme.bodySmall, ··· 417 418 ), 418 419 ); 419 420 } 420 - 421 - String _labelForStatus(FollowStatus status) { 422 - switch (status) { 423 - case FollowStatus.deleted: 424 - return 'Deleted'; 425 - case FollowStatus.deactivated: 426 - return 'Deactivated'; 427 - case FollowStatus.suspended: 428 - return 'Suspended'; 429 - case FollowStatus.blockedBy: 430 - return 'Blocked by'; 431 - case FollowStatus.blocking: 432 - return 'Blocking'; 433 - case FollowStatus.mutualBlock: 434 - return 'Mutual block'; 435 - case FollowStatus.hidden: 436 - return 'Hidden'; 437 - case FollowStatus.selfFollow: 438 - return 'Self-follow'; 439 - } 440 - } 441 421 } 442 422 443 423 class _ResultsPanel extends StatefulWidget { ··· 456 436 @override 457 437 Widget build(BuildContext context) { 458 438 if (widget.state.status == FollowAuditStatus.initial) { 459 - return const Center(child: Text('Tap Scan to audit your follow list.')); 439 + return Center(child: Text(context.l10n.messageFollowAuditStartPrompt)); 460 440 } 461 441 462 442 if (widget.state.results.isEmpty && 463 443 (widget.state.status == FollowAuditStatus.ready || widget.state.status == FollowAuditStatus.complete)) { 464 - return const Center(child: Text('No problematic follows found', key: Key('follow_audit_empty_message'))); 444 + return Center( 445 + child: Text(context.l10n.messageNoProblematicFollows, key: const Key('follow_audit_empty_message')), 446 + ); 465 447 } 466 448 467 449 if (widget.visibleEntries.isEmpty && widget.state.results.isNotEmpty) { 468 - return const Center(child: Text('No results visible for the current filters.')); 450 + return Center(child: Text(context.l10n.messageNoResultsForFilters)); 469 451 } 470 452 471 453 return ListView.separated( ··· 535 517 await Clipboard.setData(ClipboardData(text: item.record.subjectDid)); 536 518 if (context.mounted) { 537 519 ScaffoldMessenger.of(context).showSnackBar( 538 - const SnackBar( 539 - content: Text('DID copied to clipboard'), 520 + SnackBar( 521 + content: Text(context.l10n.formatDidCopied), 540 522 behavior: SnackBarBehavior.floating, 541 523 ), 542 524 ); ··· 556 538 ), 557 539 ), 558 540 const SizedBox(width: 8), 559 - Chip(label: Text(item.statusLabel), visualDensity: VisualDensity.compact), 541 + Chip(label: Text(_labelForStatus(context, item.status)), visualDensity: VisualDensity.compact), 560 542 ], 561 543 ), 562 544 ), ··· 592 574 border: Border(top: BorderSide(color: context.colorScheme.outlineVariant)), 593 575 ), 594 576 child: Text( 595 - 'Selected: $selectedCount/$total', 577 + context.l10n.formatSelectedCount(selectedCount, total), 596 578 style: context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600), 597 579 ), 598 580 ); 599 581 } 600 582 } 583 + 584 + String _labelForStatus(BuildContext context, FollowStatus status) { 585 + final l10n = context.l10n; 586 + return switch (status) { 587 + FollowStatus.deleted => l10n.statusDeleted, 588 + FollowStatus.deactivated => l10n.statusDeactivated, 589 + FollowStatus.suspended => l10n.statusSuspended, 590 + FollowStatus.blockedBy => l10n.statusBlockedBy, 591 + FollowStatus.blocking => l10n.statusBlocking, 592 + FollowStatus.mutualBlock => l10n.statusMutualBlock, 593 + FollowStatus.hidden => l10n.statusHidden, 594 + FollowStatus.selfFollow => l10n.statusSelfFollow, 595 + }; 596 + }
+39 -23
lib/features/profile/presentation/profile_connections_screen.dart
··· 1 1 import 'package:bluesky/app_bsky_actor_defs.dart'; 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter_bloc/flutter_bloc.dart'; 4 + import 'package:lazurite/core/l10n/l10n.dart'; 4 5 import 'package:lazurite/core/logging/app_logger.dart'; 5 6 import 'package:lazurite/core/theme/spacing.dart'; 6 7 import 'package:lazurite/core/theme/theme_extensions.dart'; 7 - import 'package:lazurite/features/connectivity/connectivity_helpers.dart'; 8 8 import 'package:lazurite/features/connectivity/cubit/connectivity_cubit.dart'; 9 9 import 'package:lazurite/features/profile/cubit/profile_action_cubit.dart'; 10 10 import 'package:lazurite/features/profile/cubit/profile_connections_cubit.dart'; ··· 68 68 final subtitle = widget.handle?.trim(); 69 69 return Scaffold( 70 70 appBar: AppBar( 71 - title: Text(subtitle == null || subtitle.isEmpty ? 'Connections' : '@$subtitle'), 71 + title: Text(subtitle == null || subtitle.isEmpty ? context.l10n.labelConnections : '@$subtitle'), 72 72 bottom: TabBar( 73 73 controller: _tabController, 74 74 onTap: (index) { ··· 77 77 cubit.loadTab(tab); 78 78 cubit.ensureSearchForTab(tab); 79 79 }, 80 - tabs: const [ 81 - Tab(text: 'Following'), 82 - Tab(text: 'Followers'), 83 - Tab(text: 'Mutuals'), 80 + tabs: [ 81 + Tab(text: context.l10n.labelFollowing), 82 + Tab(text: context.l10n.labelFollowers), 83 + Tab(text: context.l10n.labelMutuals), 84 84 ], 85 85 ), 86 86 ), ··· 131 131 onChanged: (query) => context.read<ProfileConnectionsCubit>().setSearchQuery(query, widget.activeTab()), 132 132 textInputAction: TextInputAction.search, 133 133 decoration: InputDecoration( 134 - hintText: 'Search handle, name, or description', 134 + hintText: context.l10n.messageSearchConnectionsPlaceholder, 135 135 prefixIcon: const Icon(Icons.search), 136 136 suffixIcon: BlocBuilder<ProfileConnectionsCubit, ProfileConnectionsState>( 137 137 buildWhen: (previous, current) => previous.searchQuery != current.searchQuery, ··· 140 140 return const SizedBox.shrink(); 141 141 } 142 142 return IconButton( 143 - tooltip: 'Clear search', 143 + tooltip: context.l10n.tooltipClearSearch, 144 144 icon: const Icon(Icons.close), 145 145 onPressed: () { 146 146 _controller.clear(); ··· 170 170 builder: (context, state) { 171 171 final data = state.dataFor(tab); 172 172 if (data.isLoading && data.profiles.isEmpty) { 173 - return LoadingState(message: 'Loading ${tab.title.toLowerCase()}...'); 173 + return LoadingState( 174 + message: context.l10n.formatConnectionsLoading(_localizedTabTitle(context, tab, lowercase: true)), 175 + ); 174 176 } 175 177 176 178 if (data.hasError && data.profiles.isEmpty) { 177 179 return ErrorState( 178 - title: 'Unable to load ${tab.title.toLowerCase()}', 179 - message: data.errorMessage ?? 'Unknown error', 180 + title: context.l10n.errorUnableToLoadConnections(_localizedTabTitle(context, tab, lowercase: true)), 181 + message: data.errorMessage ?? context.l10n.errorUnknown, 180 182 onRetry: () => context.read<ProfileConnectionsCubit>().loadTab(tab, force: true), 181 183 ); 182 184 } ··· 185 187 final isSearchMode = state.searchQuery.isNotEmpty; 186 188 if (profiles.isEmpty) { 187 189 if (isSearchMode && data.isSearching) { 188 - return LoadingState(message: 'Searching ${data.searchedCount} accounts...'); 190 + return LoadingState(message: context.l10n.formatConnectionsSearching(data.searchedCount)); 189 191 } 190 192 191 193 final message = state.searchQuery.isEmpty 192 - ? 'No ${tab.title.toLowerCase()} found' 193 - : 'No ${tab.title.toLowerCase()} match "${state.searchQuery}"'; 194 + ? context.l10n.formatConnectionsNoneFound(_localizedTabTitle(context, tab, lowercase: true)) 195 + : context.l10n.formatConnectionsNoMatches( 196 + _localizedTabTitle(context, tab, lowercase: true), 197 + state.searchQuery, 198 + ); 194 199 return EmptyState(message: message, icon: Icons.person_search_outlined); 195 200 } 196 201 ··· 295 300 Widget build(BuildContext context) { 296 301 final title = profile.displayName?.trim().isNotEmpty == true ? profile.displayName!.trim() : profile.handle; 297 302 final description = profile.description?.trim(); 298 - final joined = profile.createdAt == null ? null : 'Joined ${_formatJoinedAgo(profile.createdAt!)}'; 303 + final joined = profile.createdAt == null 304 + ? null 305 + : context.l10n.formatJoinedRelative(_formatJoinedAgo(profile.createdAt!)); 299 306 final metaStyle = context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant); 300 307 301 308 return Material( ··· 362 369 363 370 final effectiveOnPressed = isOffline || isBlockedBy ? null : onPressed; 364 371 final button = isFollowing 365 - ? OutlinedButton(onPressed: effectiveOnPressed, child: const Text('Following')) 366 - : FilledButton.tonal(onPressed: effectiveOnPressed, child: const Text('Follow')); 372 + ? OutlinedButton(onPressed: effectiveOnPressed, child: Text(context.l10n.buttonFollowing)) 373 + : FilledButton.tonal(onPressed: effectiveOnPressed, child: Text(context.l10n.buttonFollow)); 367 374 368 375 if (!isOffline) { 369 376 return button; 370 377 } 371 - return Tooltip(message: offlineActionMessage('change your follow state'), child: button); 378 + return Tooltip(message: context.l10n.formatOfflineReconnectAction('change your follow state'), child: button); 372 379 } 373 380 } 374 381 ··· 380 387 return Chip( 381 388 visualDensity: VisualDensity.compact, 382 389 side: BorderSide(color: context.colorScheme.outlineVariant), 383 - label: const Text('You'), 390 + label: Text(context.l10n.labelYou), 384 391 ); 385 392 } 386 393 } ··· 414 421 ], 415 422 OutlinedButton( 416 423 onPressed: () => context.read<ProfileConnectionsCubit>().loadMore(tab), 417 - child: Text(errorMessage == null ? 'Load more' : 'Retry'), 424 + child: Text(errorMessage == null ? context.l10n.buttonLoadMore : context.l10n.buttonRetry), 418 425 ), 419 426 ], 420 427 ), ··· 433 440 final colorScheme = context.colorScheme; 434 441 final textTheme = context.textTheme; 435 442 final message = switch (data.searchStatus) { 436 - ProfileConnectionsSearchStatus.searching => 'Searching ${data.searchedCount} accounts...', 437 - ProfileConnectionsSearchStatus.complete => 'Searched ${data.searchedCount} accounts', 438 - ProfileConnectionsSearchStatus.error => 'Search stopped after ${data.searchedCount} accounts', 443 + ProfileConnectionsSearchStatus.searching => context.l10n.formatConnectionsSearching(data.searchedCount), 444 + ProfileConnectionsSearchStatus.complete => context.l10n.formatConnectionsSearched(data.searchedCount), 445 + ProfileConnectionsSearchStatus.error => context.l10n.formatConnectionsSearchStopped(data.searchedCount), 439 446 ProfileConnectionsSearchStatus.idle => '', 440 447 }; 441 448 ··· 492 499 } 493 500 return 'now'; 494 501 } 502 + 503 + String _localizedTabTitle(BuildContext context, ProfileConnectionsTab tab, {bool lowercase = false}) { 504 + final title = switch (tab) { 505 + ProfileConnectionsTab.following => context.l10n.labelFollowing, 506 + ProfileConnectionsTab.followers => context.l10n.labelFollowers, 507 + ProfileConnectionsTab.mutuals => context.l10n.labelMutuals, 508 + }; 509 + return lowercase ? title.toLowerCase() : title; 510 + }
+49 -47
lib/features/profile/presentation/profile_context_screen.dart
··· 3 3 import 'package:flutter/material.dart'; 4 4 import 'package:flutter_bloc/flutter_bloc.dart'; 5 5 import 'package:go_router/go_router.dart'; 6 + import 'package:lazurite/core/l10n/l10n.dart'; 7 + import 'package:lazurite/core/theme/theme_extensions.dart'; 6 8 import 'package:lazurite/features/moderation/presentation/widgets/moderated_avatar.dart'; 7 9 import 'package:lazurite/features/profile/cubit/profile_context_cubit.dart'; 8 10 import 'package:lazurite/features/profile/data/profile_context_repository.dart'; 9 11 import 'package:lazurite/shared/presentation/helpers/navigation_helpers.dart'; 10 - import 'package:lazurite/core/theme/theme_extensions.dart'; 11 12 12 13 class ProfileContextScreen extends StatefulWidget { 13 14 const ProfileContextScreen({super.key, required this.handle}); ··· 67 68 title: Column( 68 69 crossAxisAlignment: CrossAxisAlignment.start, 69 70 children: [ 70 - const Text('Profile Context'), 71 + Text(context.l10n.labelProfileContext), 71 72 Text( 72 73 '@${widget.handle}', 73 74 style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurfaceVariant), ··· 77 78 bottom: TabBar( 78 79 controller: _tabController, 79 80 tabs: [ 80 - Tab(text: 'Blocked By${state.blockedByCount > 0 ? ' (${state.blockedByCount})' : ''}'), 81 - Tab(text: 'Blocking${state.blockingCount > 0 ? ' (${state.blockingCount})' : ''}'), 82 - Tab(text: 'Lists${state.listsOnCount > 0 ? ' (${state.listsOnCount})' : ''}'), 81 + Tab(text: _tabLabel(context.l10n.labelBlockedBy, state.blockedByCount)), 82 + Tab(text: _tabLabel(context.l10n.labelBlocking, state.blockingCount)), 83 + Tab(text: _tabLabel(context.l10n.labelLists, state.listsOnCount)), 83 84 ], 84 85 ), 85 86 ), ··· 119 120 child: CustomScrollView( 120 121 physics: const AlwaysScrollableScrollPhysics(), 121 122 slivers: [ 122 - const SliverToBoxAdapter( 123 + SliverToBoxAdapter( 123 124 child: Padding( 124 - padding: EdgeInsets.fromLTRB(16, 16, 16, 8), 125 - child: Text( 126 - 'Blocks are a normal part of social media. ' 127 - 'This data is public on the AT Protocol.', 128 - textAlign: TextAlign.center, 129 - ), 125 + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), 126 + child: Text(context.l10n.messageBlockedByContextNotice, textAlign: TextAlign.center), 130 127 ), 131 128 ), 132 129 SliverToBoxAdapter( ··· 135 132 child: Row( 136 133 children: [ 137 134 Text( 138 - '${state.blockedByCount} account${state.blockedByCount == 1 ? '' : 's'}', 135 + context.l10n.formatAccountCount(state.blockedByCount), 139 136 style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), 140 137 ), 141 138 const Spacer(), ··· 144 141 key: const Key('blocked_by_show_accounts'), 145 142 onPressed: () => cubit.loadBlockedBy(), 146 143 icon: const Icon(Icons.expand_more), 147 - label: const Text('Show accounts'), 144 + label: Text(context.l10n.buttonShowAccounts), 148 145 ), 149 146 ], 150 147 ), ··· 157 154 hasScrollBody: false, 158 155 child: Center( 159 156 child: _ErrorRetry( 160 - message: state.blockedByError ?? 'Failed to load accounts', 157 + message: state.blockedByError ?? context.l10n.errorFailedToLoadAccounts, 161 158 onRetry: () => cubit.loadBlockedBy(), 162 159 ), 163 160 ), ··· 168 165 child: Center( 169 166 child: Text( 170 167 state.blockedByCount > 0 171 - ? 'Found ${state.blockedByCount} blocked-by accounts, but public Bluesky profile details could not be loaded.' 172 - : 'No accounts have blocked this user', 168 + ? context.l10n.formatBlockedByAccountsUnavailable(state.blockedByCount) 169 + : context.l10n.messageNoAccountsBlockedThisUser, 173 170 textAlign: TextAlign.center, 174 171 ), 175 172 ), ··· 191 188 return _UnavailableProfileTile( 192 189 key: ValueKey('blocked_by_unavailable_${entry.did}'), 193 190 did: entry.did, 194 - reason: entry.unavailableReason ?? 'Profile unavailable', 191 + reason: entry.unavailableReason ?? context.l10n.messageProfileUnavailable, 195 192 ); 196 193 }, 197 194 ), ··· 207 204 child: Padding( 208 205 padding: const EdgeInsets.all(16), 209 206 child: _ErrorRetry( 210 - message: state.blockedByError ?? 'Failed to load more', 207 + message: state.blockedByError ?? context.l10n.errorFailedToLoadMore, 211 208 onRetry: () => cubit.loadBlockedBy(cursor: state.blockedByCursor), 212 209 ), 213 210 ), ··· 230 227 final cubit = context.read<ProfileContextCubit>(); 231 228 232 229 if (!state.isOwnProfile) { 233 - return const Center( 230 + return Center( 234 231 child: Padding( 235 - padding: EdgeInsets.all(24), 236 - child: Text( 237 - 'Blocking information is only available when viewing your own profile.', 238 - textAlign: TextAlign.center, 239 - ), 232 + padding: const EdgeInsets.all(24), 233 + child: Text(context.l10n.messageBlockingOnlyOwnProfile, textAlign: TextAlign.center), 240 234 ), 241 235 ); 242 236 } ··· 258 252 child: Padding( 259 253 padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), 260 254 child: Text( 261 - '${state.blockingCount} account${state.blockingCount == 1 ? '' : 's'}', 255 + context.l10n.formatAccountCount(state.blockingCount), 262 256 style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), 263 257 ), 264 258 ), ··· 278 272 hasScrollBody: false, 279 273 child: Center( 280 274 child: _ErrorRetry( 281 - message: state.blockingError ?? 'Failed to load accounts', 275 + message: state.blockingError ?? context.l10n.errorFailedToLoadAccounts, 282 276 onRetry: () => cubit.loadBlocking(), 283 277 ), 284 278 ), ··· 289 283 child: Center( 290 284 child: Text( 291 285 state.blockingUnavailable.isNotEmpty 292 - ? 'Some blocked accounts are suspended or unavailable.' 293 - : 'Not blocking anyone', 286 + ? context.l10n.messageSomeBlockedAccountsUnavailable 287 + : context.l10n.messageNotBlockingAnyone, 294 288 textAlign: TextAlign.center, 295 289 ), 296 290 ), ··· 319 313 child: Padding( 320 314 padding: const EdgeInsets.all(16), 321 315 child: _ErrorRetry( 322 - message: state.blockingError ?? 'Failed to load more', 316 + message: state.blockingError ?? context.l10n.errorFailedToLoadMore, 323 317 onRetry: () => cubit.loadBlocking(cursor: state.blockingCursor), 324 318 ), 325 319 ), ··· 362 356 hasScrollBody: false, 363 357 child: Center( 364 358 child: _ErrorRetry( 365 - message: state.listsOnError ?? 'Failed to load lists', 359 + message: state.listsOnError ?? context.l10n.errorFailedToLoadLists, 366 360 onRetry: () => cubit.loadListsOn(), 367 361 ), 368 362 ), 369 363 ) 370 364 else if (state.listsOnStatus == ProfileContextTabStatus.loaded && state.listsOn.isEmpty) 371 - const SliverFillRemaining(hasScrollBody: false, child: Center(child: Text('Not on any lists'))) 365 + SliverFillRemaining(hasScrollBody: false, child: Center(child: Text(context.l10n.messageNotOnAnyLists))) 372 366 else ...[ 373 367 ..._buildListSections( 374 368 context, ··· 387 381 child: Padding( 388 382 padding: const EdgeInsets.all(16), 389 383 child: _ErrorRetry( 390 - message: state.listsOnError ?? 'Failed to load more', 384 + message: state.listsOnError ?? context.l10n.errorFailedToLoadMore, 391 385 onRetry: () => cubit.loadListsOn(cursor: state.listsOnCursor), 392 386 ), 393 387 ), ··· 444 438 children: [ 445 439 ListTile( 446 440 leading: const Icon(Icons.warning_amber_rounded), 447 - title: Text('Unavailable accounts (${entries.length})'), 448 - subtitle: const Text('These accounts are suspended or their public profile could not be fetched.'), 441 + title: Text(context.l10n.formatUnavailableAccounts(entries.length)), 442 + subtitle: Text(context.l10n.messageUnavailableAccountsDescription), 449 443 ), 450 444 for (final entry in entries) 451 445 ListTile( ··· 507 501 List<bsky_graph.ListView> lists, 508 502 ValueChanged<bsky_graph.ListView> onTap, 509 503 ) { 510 - final sections = _groupListsByPurpose(lists); 504 + final sections = _groupListsByPurpose(context, lists); 511 505 return [ 512 506 for (final section in sections) ...[ 513 507 SliverToBoxAdapter( ··· 527 521 ]; 528 522 } 529 523 530 - List<({String title, List<bsky_graph.ListView> lists})> _groupListsByPurpose(List<bsky_graph.ListView> lists) { 524 + List<({String title, List<bsky_graph.ListView> lists})> _groupListsByPurpose( 525 + BuildContext context, 526 + List<bsky_graph.ListView> lists, 527 + ) { 531 528 final buckets = <_ListPurposeGroup, List<bsky_graph.ListView>>{ 532 529 _ListPurposeGroup.curation: [], 533 530 _ListPurposeGroup.moderation: [], ··· 540 537 } 541 538 542 539 return [ 543 - (title: 'Curation Lists', lists: buckets[_ListPurposeGroup.curation]!), 544 - (title: 'Moderation Lists', lists: buckets[_ListPurposeGroup.moderation]!), 545 - (title: 'Reference Lists', lists: buckets[_ListPurposeGroup.reference]!), 546 - (title: 'Other Lists', lists: buckets[_ListPurposeGroup.other]!), 540 + (title: context.l10n.labelCurationLists, lists: buckets[_ListPurposeGroup.curation]!), 541 + (title: context.l10n.labelModerationLists, lists: buckets[_ListPurposeGroup.moderation]!), 542 + (title: context.l10n.labelReferenceLists, lists: buckets[_ListPurposeGroup.reference]!), 543 + (title: context.l10n.labelOtherLists, lists: buckets[_ListPurposeGroup.other]!), 547 544 ].where((section) => section.lists.isNotEmpty).toList(); 548 545 } 549 546 ··· 561 558 } 562 559 563 560 enum _ListPurposeGroup { curation, moderation, reference, other } 561 + 562 + String _tabLabel(String label, int count) => count > 0 ? '$label ($count)' : label; 564 563 565 564 class _ListContextCard extends StatelessWidget { 566 565 const _ListContextCard({super.key, required this.list, this.onTap}); ··· 616 615 const SizedBox(height: 12), 617 616 _ListMetaChip( 618 617 icon: Icons.group_outlined, 619 - label: '${list.listItemCount ?? 0} member${(list.listItemCount ?? 0) == 1 ? '' : 's'}', 618 + label: context.l10n.formatMemberCount(list.listItemCount ?? 0), 620 619 ), 621 620 if (description != null && description.isNotEmpty) ...[ 622 621 const SizedBox(height: 12), ··· 640 639 Widget build(BuildContext context) { 641 640 final colorScheme = context.colorScheme; 642 641 final (label, color) = switch (purpose) { 643 - bsky_graph.KnownListPurpose.appBskyGraphDefsCuratelist => ('CURATE', colorScheme.primary), 644 - bsky_graph.KnownListPurpose.appBskyGraphDefsModlist => ('MOD', colorScheme.error), 645 - bsky_graph.KnownListPurpose.appBskyGraphDefsReferencelist => ('REFERENCE', colorScheme.tertiary), 646 - null => ('LIST', colorScheme.secondary), 642 + bsky_graph.KnownListPurpose.appBskyGraphDefsCuratelist => (context.l10n.labelCurateShort, colorScheme.primary), 643 + bsky_graph.KnownListPurpose.appBskyGraphDefsModlist => (context.l10n.labelModerationShort, colorScheme.error), 644 + bsky_graph.KnownListPurpose.appBskyGraphDefsReferencelist => ( 645 + context.l10n.labelReferenceShort, 646 + colorScheme.tertiary, 647 + ), 648 + null => (context.l10n.labelList.toUpperCase(), colorScheme.secondary), 647 649 }; 648 650 649 651 return Container( ··· 788 790 children: [ 789 791 Text(message, textAlign: TextAlign.center), 790 792 const SizedBox(height: 12), 791 - FilledButton(onPressed: onRetry, child: const Text('Retry')), 793 + FilledButton(onPressed: onRetry, child: Text(context.l10n.buttonRetry)), 792 794 ], 793 795 ); 794 796 }
+36 -23
lib/features/profile/presentation/profile_edit_screen.dart
··· 7 7 import 'package:flutter_bloc/flutter_bloc.dart'; 8 8 import 'package:go_router/go_router.dart'; 9 9 import 'package:image_picker/image_picker.dart'; 10 + import 'package:lazurite/core/l10n/l10n.dart'; 10 11 import 'package:lazurite/core/logging/app_logger.dart'; 11 12 import 'package:lazurite/core/theme/animation_tokens.dart'; 12 13 import 'package:lazurite/core/theme/animation_utils.dart'; ··· 91 92 final size = await image.length(); 92 93 if (size > ProfileImageUpload.maxBytes) { 93 94 if (mounted) { 94 - showAppSnackBar(context, 'Image must be smaller than 1MB', isError: true); 95 + showAppSnackBar(context, context.l10n.errorImageTooLarge, isError: true); 95 96 } 96 97 return; 97 98 } ··· 99 100 final mimeType = profileImageMimeTypeFor(reportedMimeType: image.mimeType, path: image.path); 100 101 if (mimeType == null) { 101 102 if (mounted) { 102 - showAppSnackBar(context, 'Use a JPEG or PNG image', isError: true); 103 + showAppSnackBar(context, context.l10n.errorInvalidProfileImageType, isError: true); 103 104 } 104 105 return; 105 106 } ··· 119 120 } catch (error, stackTrace) { 120 121 log.w('ProfileEditScreen: failed to pick profile image', error: error, stackTrace: stackTrace); 121 122 if (mounted) { 122 - showAppSnackBar(context, 'Unable to read selected image', isError: true); 123 + showAppSnackBar(context, context.l10n.errorProfileImageReadFailed, isError: true); 123 124 } 124 125 } finally { 125 126 if (mounted) { ··· 160 161 return; 161 162 } 162 163 context.read<ProfileBloc>().add(ProfileLoadRequested(actor: did)); 163 - showAppSnackBar(context, 'Profile updated', behavior: SnackBarBehavior.floating); 164 + showAppSnackBar(context, context.l10n.messageProfileUpdated, behavior: SnackBarBehavior.floating); 164 165 context.go('/profile/me'); 165 166 } catch (error, stackTrace) { 166 167 log.w('ProfileEditScreen: failed to save profile', error: error, stackTrace: stackTrace); 167 168 if (mounted) { 168 - showAppSnackBar(context, 'Unable to update profile', isError: true); 169 + showAppSnackBar(context, context.l10n.errorUnableToUpdateProfile, isError: true); 169 170 } 170 171 } finally { 171 172 if (mounted) { ··· 197 198 return null; 198 199 } 199 200 if (text.characters.length > maxGraphemes) { 200 - return '$label must be $maxGraphemes characters or fewer'; 201 + return context.l10n.formatProfileTextLimit(label, maxGraphemes); 201 202 } 202 203 if (utf8.encode(text).length > maxUtf8Bytes) { 203 - return '$label is too long'; 204 + return context.l10n.formatProfileTextTooLong(label); 204 205 } 205 206 return null; 206 207 } ··· 212 213 } 213 214 final uri = Uri.tryParse(normalized); 214 215 if (uri == null || !uri.hasScheme || uri.host.isEmpty || (uri.scheme != 'http' && uri.scheme != 'https')) { 215 - return 'Enter a valid website'; 216 + return context.l10n.validationEnterValidWebsite; 216 217 } 217 218 return null; 218 219 } ··· 227 228 return AppScreenEntrance( 228 229 child: Scaffold( 229 230 appBar: AppBar( 230 - title: const Text('Edit profile'), 231 + title: Text(context.l10n.labelEditProfile), 231 232 leading: IconButton( 232 233 icon: const Icon(Icons.close), 233 - tooltip: 'Cancel', 234 + tooltip: context.l10n.buttonCancel, 234 235 onPressed: _saving ? null : () => context.go('/profile/me'), 235 236 ), 236 237 actions: [ ··· 240 241 icon: _saving 241 242 ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) 242 243 : const Icon(Icons.check), 243 - label: const Text('Save'), 244 + label: Text(context.l10n.buttonSave), 244 245 ), 245 246 ], 246 247 ), ··· 279 280 crossAxisAlignment: CrossAxisAlignment.start, 280 281 children: [ 281 282 Tooltip( 282 - message: 'Change banner image', 283 + message: context.l10n.messageChangeBannerImage, 283 284 child: Semantics( 284 - label: 'Change banner image', 285 + label: context.l10n.messageChangeBannerImage, 285 286 button: true, 286 287 child: AspectRatio( 287 288 aspectRatio: 3, ··· 302 303 icon: _pickingBanner 303 304 ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) 304 305 : const Icon(Icons.image_outlined), 305 - label: const Text('Banner'), 306 + label: Text(context.l10n.labelBanner), 306 307 ), 307 308 ), 308 309 ], ··· 317 318 child: Padding( 318 319 padding: const EdgeInsets.only(left: 16), 319 320 child: Tooltip( 320 - message: 'Change avatar image', 321 + message: context.l10n.messageChangeAvatarImage, 321 322 child: Semantics( 322 - label: 'Change avatar image', 323 + label: context.l10n.messageChangeAvatarImage, 323 324 button: true, 324 325 child: InkWell( 325 326 key: const ValueKey('profile_edit_avatar_picker'), ··· 404 405 key: const ValueKey('profile_edit_display_name_field'), 405 406 controller: _displayNameController, 406 407 textInputAction: TextInputAction.next, 407 - decoration: const InputDecoration(labelText: 'Display name', prefixIcon: Icon(Icons.badge_outlined)), 408 - validator: (value) => _validateTextLimit(value, 'Display name', maxGraphemes: 64, maxUtf8Bytes: 640), 408 + decoration: InputDecoration( 409 + labelText: context.l10n.labelDisplayName, 410 + prefixIcon: const Icon(Icons.badge_outlined), 411 + ), 412 + validator: (value) => 413 + _validateTextLimit(value, context.l10n.labelDisplayName, maxGraphemes: 64, maxUtf8Bytes: 640), 409 414 ), 410 415 const SizedBox(height: 16), 411 416 TextFormField( ··· 414 419 minLines: 3, 415 420 maxLines: 6, 416 421 textInputAction: TextInputAction.newline, 417 - decoration: const InputDecoration(labelText: 'Description', prefixIcon: Icon(Icons.notes_outlined)), 418 - validator: (value) => _validateTextLimit(value, 'Description', maxGraphemes: 256, maxUtf8Bytes: 2560), 422 + decoration: InputDecoration( 423 + labelText: context.l10n.labelDescription, 424 + prefixIcon: const Icon(Icons.notes_outlined), 425 + ), 426 + validator: (value) => 427 + _validateTextLimit(value, context.l10n.labelDescription, maxGraphemes: 256, maxUtf8Bytes: 2560), 419 428 ), 420 429 const SizedBox(height: 16), 421 430 TextFormField( 422 431 key: const ValueKey('profile_edit_pronouns_field'), 423 432 controller: _pronounsController, 424 433 textInputAction: TextInputAction.next, 425 - decoration: const InputDecoration(labelText: 'Pronouns', prefixIcon: Icon(Icons.record_voice_over_outlined)), 426 - validator: (value) => _validateTextLimit(value, 'Pronouns', maxGraphemes: 20, maxUtf8Bytes: 200), 434 + decoration: InputDecoration( 435 + labelText: context.l10n.labelPronouns, 436 + prefixIcon: const Icon(Icons.record_voice_over_outlined), 437 + ), 438 + validator: (value) => 439 + _validateTextLimit(value, context.l10n.labelPronouns, maxGraphemes: 20, maxUtf8Bytes: 200), 427 440 ), 428 441 const SizedBox(height: 16), 429 442 TextFormField( ··· 431 444 controller: _websiteController, 432 445 keyboardType: TextInputType.url, 433 446 textInputAction: TextInputAction.done, 434 - decoration: const InputDecoration(labelText: 'Website', prefixIcon: Icon(Icons.link_outlined)), 447 + decoration: InputDecoration(labelText: context.l10n.labelWebsite, prefixIcon: const Icon(Icons.link_outlined)), 435 448 validator: _validateWebsite, 436 449 onFieldSubmitted: (_) { 437 450 final profile = context.read<ProfileBloc>().state.profile;
+81 -90
lib/features/profile/presentation/profile_screen.dart
··· 8 8 import 'package:flutter_bloc/flutter_bloc.dart'; 9 9 import 'package:go_router/go_router.dart'; 10 10 import 'package:intl/intl.dart'; 11 + import 'package:lazurite/core/l10n/l10n.dart'; 11 12 import 'package:lazurite/core/logging/app_logger.dart'; 12 13 import 'package:lazurite/core/network/app_view_provider.dart'; 13 14 import 'package:lazurite/core/network/app_view_web_links.dart'; ··· 21 22 import 'package:lazurite/core/widgets/sliver_tab_bar_delegate.dart'; 22 23 import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 23 24 import 'package:lazurite/features/compose/presentation/compose_route_args.dart'; 24 - import 'package:lazurite/features/connectivity/connectivity_helpers.dart'; 25 25 import 'package:lazurite/features/connectivity/cubit/connectivity_cubit.dart'; 26 26 import 'package:lazurite/features/feed/bloc/feed_bloc.dart'; 27 27 import 'package:lazurite/features/feed/presentation/widgets/facet_text.dart'; ··· 58 58 enum _ProfileFeedSlice { posts, replies, quotes, reposts, media } 59 59 60 60 class _ProfileFeedTabConfig { 61 - const _ProfileFeedTabConfig({ 62 - required this.label, 63 - required this.requestFilter, 64 - required this.slice, 65 - required this.emptyLabel, 66 - }); 61 + const _ProfileFeedTabConfig({required this.requestFilter, required this.slice}); 67 62 68 - final String label; 69 63 final FeedFilter requestFilter; 70 64 final _ProfileFeedSlice slice; 71 - final String emptyLabel; 72 65 } 73 66 74 67 class ProfileScreen extends StatefulWidget { ··· 83 76 84 77 class _ProfileScreenState extends State<ProfileScreen> with TickerProviderStateMixin { 85 78 static const _feedTabs = [ 86 - _ProfileFeedTabConfig( 87 - label: 'Posts', 88 - requestFilter: FeedFilter.postsNoReplies, 89 - slice: _ProfileFeedSlice.posts, 90 - emptyLabel: 'No posts yet', 91 - ), 92 - _ProfileFeedTabConfig( 93 - label: 'Replies', 94 - requestFilter: FeedFilter.postsWithReplies, 95 - slice: _ProfileFeedSlice.replies, 96 - emptyLabel: 'No replies yet', 97 - ), 98 - _ProfileFeedTabConfig( 99 - label: 'Quotes', 100 - requestFilter: FeedFilter.postsWithReplies, 101 - slice: _ProfileFeedSlice.quotes, 102 - emptyLabel: 'No quotes yet', 103 - ), 104 - _ProfileFeedTabConfig( 105 - label: 'Reposts', 106 - requestFilter: FeedFilter.postsWithReplies, 107 - slice: _ProfileFeedSlice.reposts, 108 - emptyLabel: 'No reposts yet', 109 - ), 110 - _ProfileFeedTabConfig( 111 - label: 'Media', 112 - requestFilter: FeedFilter.postsWithMedia, 113 - slice: _ProfileFeedSlice.media, 114 - emptyLabel: 'No media posts yet', 115 - ), 79 + _ProfileFeedTabConfig(requestFilter: FeedFilter.postsNoReplies, slice: _ProfileFeedSlice.posts), 80 + _ProfileFeedTabConfig(requestFilter: FeedFilter.postsWithReplies, slice: _ProfileFeedSlice.replies), 81 + _ProfileFeedTabConfig(requestFilter: FeedFilter.postsWithReplies, slice: _ProfileFeedSlice.quotes), 82 + _ProfileFeedTabConfig(requestFilter: FeedFilter.postsWithReplies, slice: _ProfileFeedSlice.reposts), 83 + _ProfileFeedTabConfig(requestFilter: FeedFilter.postsWithMedia, slice: _ProfileFeedSlice.media), 116 84 ]; 117 85 118 - static const _baseTabLabelsOwn = ['POSTS', 'REPLIES', 'QUOTES', 'REPOSTS', 'MEDIA', 'LISTS', 'STARTER PACKS']; 119 - static const _baseTabLabelsOther = [ 120 - 'POSTS', 121 - 'REPLIES', 122 - 'QUOTES', 123 - 'REPOSTS', 124 - 'MEDIA', 125 - 'LIKED', 126 - 'LISTS', 127 - 'STARTER PACKS', 128 - ]; 129 - static const _suggestedTabLabel = 'SUGGESTED'; 86 + static const _baseTabCountOwn = 7; 87 + static const _baseTabCountOther = 8; 130 88 static const _coverRefreshTriggerDistance = 72.0; 131 89 132 90 late TabController _tabController; ··· 151 109 void initState() { 152 110 super.initState(); 153 111 _showSuggestedTab = _shouldShowSuggestedTab(context.read<ProfileBloc>().state.profile); 154 - _tabController = TabController(length: _tabLabels.length, vsync: this); 112 + _tabController = TabController(length: _tabCount, vsync: this); 155 113 _loadProfileAndFeed(); 156 114 } 157 115 ··· 312 270 }); 313 271 } 314 272 315 - List<String> get _baseTabLabels => _showSuggestedTab ? _baseTabLabelsOther : _baseTabLabelsOwn; 273 + int get _baseTabCount => _showSuggestedTab ? _baseTabCountOther : _baseTabCountOwn; 316 274 317 - List<String> get _tabLabels => 318 - _showSuggestedTab ? [..._baseTabLabels, _suggestedTabLabel] : List<String>.of(_baseTabLabels); 275 + int get _tabCount => _baseTabCount + (_showSuggestedTab ? 1 : 0); 276 + 277 + List<String> _localizedTabLabels(BuildContext context) { 278 + final l10n = context.l10n; 279 + final labels = [ 280 + l10n.labelPosts.toUpperCase(), 281 + l10n.labelReplies.toUpperCase(), 282 + l10n.labelQuotes.toUpperCase(), 283 + l10n.labelReposts.toUpperCase(), 284 + l10n.labelMedia.toUpperCase(), 285 + if (_showSuggestedTab) l10n.labelLiked.toUpperCase(), 286 + l10n.labelLists.toUpperCase(), 287 + l10n.labelStarterPacks.toUpperCase(), 288 + if (_showSuggestedTab) l10n.labelSuggested.toUpperCase(), 289 + ]; 290 + return labels; 291 + } 292 + 293 + String _emptyLabelForSlice(BuildContext context, _ProfileFeedSlice slice) { 294 + final l10n = context.l10n; 295 + return switch (slice) { 296 + _ProfileFeedSlice.posts => l10n.messageNoPostsYet, 297 + _ProfileFeedSlice.replies => l10n.messageNoRepliesYet, 298 + _ProfileFeedSlice.quotes => l10n.messageNoQuotesYet, 299 + _ProfileFeedSlice.reposts => l10n.messageNoRepostsYet, 300 + _ProfileFeedSlice.media => l10n.messageNoMediaPostsYet, 301 + }; 302 + } 319 303 320 304 _ProfileFeedTabConfig get _currentFeedTab => 321 305 _feedTabs[_tabController.index < _feedTabs.length ? _tabController.index : 0]; ··· 362 346 return; 363 347 } 364 348 365 - final maxIndex = show ? _baseTabLabelsOther.length : _baseTabLabelsOwn.length - 1; 349 + final maxIndex = show ? _baseTabCountOther : _baseTabCountOwn - 1; 366 350 final nextIndex = _tabController.index.clamp(0, maxIndex); 367 351 final previousController = _tabController; 368 352 _showSuggestedTab = show; 369 - _tabController = TabController(length: _tabLabels.length, vsync: this, initialIndex: nextIndex); 353 + _tabController = TabController(length: _tabCount, vsync: this, initialIndex: nextIndex); 370 354 setState(() {}); 371 355 372 356 WidgetsBinding.instance.addPostFrameCallback((_) { ··· 376 360 377 361 String _appBarTitle(ProfileViewDetailed? profile) { 378 362 final authState = context.read<AuthBloc>().state; 379 - return profile?.displayName ?? profile?.handle ?? widget.actor ?? authState.tokens?.handle ?? 'Profile'; 363 + return profile?.displayName ?? 364 + profile?.handle ?? 365 + widget.actor ?? 366 + authState.tokens?.handle ?? 367 + context.l10n.labelProfileTitle; 380 368 } 381 369 382 370 void _openProfilePostSearch(BuildContext context, ProfileViewDetailed profile) { ··· 522 510 sourceState: stateForTab, 523 511 requestFilter: tab.requestFilter, 524 512 slice: tab.slice, 525 - emptyLabel: tab.emptyLabel, 513 + emptyLabel: _emptyLabelForSlice(context, tab.slice), 526 514 profile: actorScopedProfile, 527 515 expectedActor: expectedActor, 528 516 ), ··· 587 575 IconButton( 588 576 key: const Key('profile_edit_header_button'), 589 577 icon: const Icon(Icons.edit_outlined), 590 - tooltip: 'Edit profile', 578 + tooltip: context.l10n.labelEditProfile, 591 579 onPressed: () => context.push('/profile/me/edit'), 592 580 ), 593 581 if (actorScopedProfile != null && isOwnProfile) ··· 600 588 IconButton( 601 589 key: const Key('profile_search_posts_button'), 602 590 icon: const Icon(Icons.search), 603 - tooltip: 'Search this profile\'s posts', 591 + tooltip: context.l10n.messageSearchThisProfilesPostsPlaceholder, 604 592 onPressed: () => _openProfilePostSearch(context, actorScopedProfile), 605 593 ), 606 594 IconButton( ··· 632 620 delegate: SliverTabBarDelegate( 633 621 TabBar( 634 622 controller: _tabController, 635 - tabs: [for (final label in _tabLabels) Tab(text: label)], 623 + tabs: [for (final label in _localizedTabLabels(context)) Tab(text: label)], 636 624 onTap: (index) { 637 625 if (index < _feedTabs.length) { 638 626 _loadFeedOnly(filter: _feedTabs[index].requestFilter); ··· 675 663 final jumpToTopButton = FloatingActionButton.small( 676 664 key: const ValueKey('profile-jump-top-fab'), 677 665 heroTag: 'profile-jump-top-fab', 678 - tooltip: 'Jump to top', 666 + tooltip: context.l10n.tooltipJumpToTop, 679 667 onPressed: _jumpToTop, 680 668 backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.9), 681 669 foregroundColor: Theme.of(context).colorScheme.onSurfaceVariant, ··· 775 763 child: Column( 776 764 crossAxisAlignment: CrossAxisAlignment.start, 777 765 children: [ 778 - Text('Unable to load profile', style: context.textTheme.titleLarge), 766 + Text(context.l10n.errorFailedToLoadProfile, style: context.textTheme.titleLarge), 779 767 const SizedBox(height: 8), 780 - Text(errorMessage ?? 'Unknown error', style: context.textTheme.bodyMedium), 768 + Text(errorMessage ?? context.l10n.errorUnknown, style: context.textTheme.bodyMedium), 781 769 const SizedBox(height: 12), 782 - FilledButton(onPressed: _loadProfileAndFeed, child: const Text('Try again')), 770 + FilledButton(onPressed: _loadProfileAndFeed, child: Text(context.l10n.buttonTryAgain)), 783 771 ], 784 772 ), 785 773 ); ··· 809 797 _buildMetaChip( 810 798 context, 811 799 Icons.calendar_today_outlined, 812 - 'Joined ${DateFormat.yMMMM().format(profile.createdAt!)}', 800 + context.l10n.formatJoinedDate(DateFormat.yMMMM().format(profile.createdAt!)), 813 801 ), 814 802 ]; 815 803 ··· 866 854 _buildStat( 867 855 context, 868 856 profile.followsCount ?? 0, 869 - 'Following', 857 + context.l10n.labelFollowing, 870 858 key: const ValueKey('profile_following_stat'), 871 859 onTap: () => _openConnections(context, profile, ProfileConnectionsTab.following), 872 860 ), ··· 874 862 _buildStat( 875 863 context, 876 864 profile.followersCount ?? 0, 877 - 'Followers', 865 + context.l10n.labelFollowers, 878 866 key: const ValueKey('profile_followers_stat'), 879 867 onTap: () => _openConnections(context, profile, ProfileConnectionsTab.followers), 880 868 ), 881 869 const SizedBox(width: 24), 882 - _buildStat(context, profile.postsCount ?? 0, 'Posts'), 870 + _buildStat(context, profile.postsCount ?? 0, context.l10n.labelPosts), 883 871 ], 884 872 ), 885 873 ), ··· 892 880 OutlinedButton.icon( 893 881 onPressed: () => context.push('/bookmarks'), 894 882 icon: const Icon(Icons.bookmark_outline), 895 - label: const Text('Bookmarks'), 883 + label: Text(context.l10n.labelBookmarks), 896 884 ), 897 885 OutlinedButton.icon( 898 886 onPressed: () => context.push('/liked'), 899 887 icon: const Icon(Icons.favorite_outline), 900 - label: const Text('Liked'), 888 + label: Text(context.l10n.labelLiked), 901 889 ), 902 890 ], 903 891 ), ··· 1024 1012 items: [ 1025 1013 OptionsSheetItem( 1026 1014 leading: const Icon(Icons.hub_outlined), 1027 - title: 'Profile Context', 1015 + title: context.l10n.labelProfileContext, 1028 1016 onTap: () => context.push( 1029 1017 '/profile-context?did=${Uri.encodeComponent(profile.did)}&handle=${Uri.encodeComponent(profile.handle)}', 1030 1018 ), 1031 1019 ), 1032 1020 OptionsSheetItem( 1033 1021 leading: const Icon(Icons.cleaning_services_outlined), 1034 - title: 'Clean Follows', 1022 + title: context.l10n.labelCleanFollows, 1035 1023 onTap: () => context.push('/settings/clean-follows'), 1036 1024 ), 1037 1025 ], ··· 1044 1032 items: [ 1045 1033 OptionsSheetItem( 1046 1034 leading: const Icon(Icons.copy), 1047 - title: 'Copy DID', 1035 + title: context.l10n.labelCopyDid, 1048 1036 onTap: () { 1049 1037 Clipboard.setData(ClipboardData(text: profile.did)); 1050 - showAppSnackBar(context, 'DID copied to clipboard', behavior: SnackBarBehavior.floating); 1038 + showAppSnackBar(context, context.l10n.formatDidCopied, behavior: SnackBarBehavior.floating); 1051 1039 }, 1052 1040 ), 1053 1041 OptionsSheetItem( 1054 1042 leading: const Icon(Icons.share_outlined), 1055 - title: 'Share Profile', 1043 + title: context.l10n.labelShareProfile, 1056 1044 onTap: () => ShareHelper.shareText( 1057 1045 context, 1058 1046 AppViewWebLinks.profile(profile.handle, appViewProvider: _resolveAppViewProvider(context)), ··· 1060 1048 ), 1061 1049 OptionsSheetItem( 1062 1050 leading: const Icon(Icons.playlist_add_outlined), 1063 - title: 'Add to list', 1051 + title: context.l10n.labelAddToList, 1064 1052 onTap: () => _showAddToList(context, profile), 1065 1053 ), 1066 1054 OptionsSheetItem( 1067 1055 leading: const Icon(Icons.people_outline), 1068 - title: 'Suggested Follows', 1056 + title: context.l10n.labelSuggestedFollows, 1069 1057 onTap: () => _showSuggestedFollows(context, profile), 1070 1058 ), 1071 1059 OptionsSheetItem( 1072 1060 leading: const Icon(Icons.hub_outlined), 1073 - title: 'Profile Context', 1061 + title: context.l10n.labelProfileContext, 1074 1062 onTap: () => context.push( 1075 1063 '/profile-context?did=${Uri.encodeComponent(profile.did)}&handle=${Uri.encodeComponent(profile.handle)}', 1076 1064 ), ··· 1106 1094 children: [ 1107 1095 Padding( 1108 1096 padding: const EdgeInsets.symmetric(vertical: 12), 1109 - child: Text('Add to list', style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)), 1097 + child: Text( 1098 + context.l10n.labelAddToList, 1099 + style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), 1100 + ), 1110 1101 ), 1111 1102 const Divider(height: 1), 1112 1103 Expanded( ··· 1117 1108 } 1118 1109 1119 1110 if (state.status == AddToListStatus.error) { 1120 - return Center(child: Text(state.errorMessage ?? 'Failed to load lists')); 1111 + return Center(child: Text(state.errorMessage ?? context.l10n.errorFailedToLoadLists)); 1121 1112 } 1122 1113 1123 1114 if (state.lists.isEmpty) { 1124 - return const Center(child: Text('No lists yet')); 1115 + return Center(child: Text(context.l10n.messageNoListsYet)); 1125 1116 } 1126 1117 1127 1118 return ListView.builder( ··· 1215 1206 return FloatingActionButton( 1216 1207 key: const ValueKey('profile-compose-fab'), 1217 1208 heroTag: 'profile-compose-fab', 1218 - tooltip: isOffline ? offlineActionMessage('compose a post') : 'Compose', 1209 + tooltip: isOffline ? context.l10n.formatOfflineReconnectAction('compose a post') : context.l10n.buttonCompose, 1219 1210 onPressed: isOffline 1220 1211 ? null 1221 1212 : () => context.push('/compose', extra: ComposeRouteArgs(initialText: initialText)), ··· 1263 1254 child: Column( 1264 1255 mainAxisSize: MainAxisSize.min, 1265 1256 children: [ 1266 - Text(sourceState.errorMessage ?? 'Failed to load posts'), 1257 + Text(sourceState.errorMessage ?? context.l10n.errorFailedToLoadPosts), 1267 1258 const SizedBox(height: 12), 1268 1259 FilledButton( 1269 1260 onPressed: () => _loadFeedOnly(filter: requestFilter), 1270 - child: const Text('Retry'), 1261 + child: Text(context.l10n.buttonRetry), 1271 1262 ), 1272 1263 ], 1273 1264 ), ··· 1605 1596 Widget build(BuildContext context) { 1606 1597 final cubit = _cubit; 1607 1598 if (cubit == null) { 1608 - return const Center(child: Text('Suggested follows are unavailable right now.')); 1599 + return Center(child: Text(context.l10n.messageSuggestedFollowsUnavailable)); 1609 1600 } 1610 1601 1611 1602 return BlocProvider.value( ··· 1666 1657 child: Column( 1667 1658 mainAxisSize: MainAxisSize.min, 1668 1659 children: [ 1669 - Text(state.errorMessage ?? 'Failed to load lists'), 1660 + Text(state.errorMessage ?? context.l10n.errorFailedToLoadLists), 1670 1661 const SizedBox(height: 12), 1671 - FilledButton(onPressed: () => _cubit.refresh(), child: const Text('Retry')), 1662 + FilledButton(onPressed: () => _cubit.refresh(), child: Text(context.l10n.buttonRetry)), 1672 1663 ], 1673 1664 ), 1674 1665 ); ··· 1682 1673 .toList(growable: false); 1683 1674 1684 1675 if (lists.isEmpty) { 1685 - return const Center(child: Text('No lists yet')); 1676 + return Center(child: Text(context.l10n.messageNoListsYet)); 1686 1677 } 1687 1678 1688 1679 return RefreshIndicator(
+42 -36
lib/features/profile/presentation/widgets/profile_action_buttons.dart
··· 1 1 import 'package:lazurite/core/theme/theme_extensions.dart'; 2 2 3 3 import 'package:flutter/material.dart'; 4 - import 'package:lazurite/features/connectivity/connectivity_helpers.dart'; 4 + import 'package:lazurite/core/l10n/l10n.dart'; 5 5 import 'package:lazurite/shared/presentation/helpers/haptic_helper.dart'; 6 6 import 'package:lazurite/shared/presentation/widgets/confirmation_dialog.dart'; 7 7 ··· 56 56 } 57 57 58 58 Widget _buildFollowButton(BuildContext context) { 59 + final l10n = context.l10n; 59 60 if (isBlocked) { 60 61 return _ActionButton( 61 - label: 'Unblock', 62 + label: l10n.buttonUnblock, 62 63 onPressed: isOffline || onUnblock == null ? null : () => _confirmUnblock(context), 63 64 isLoading: isLoadingBlock, 64 65 foregroundColor: context.colorScheme.onError, 65 66 backgroundColor: context.colorScheme.error, 66 - tooltip: isOffline ? offlineActionMessage('unblock this account') : null, 67 + tooltip: isOffline ? l10n.formatOfflineReconnectAction(l10n.buttonUnblock.toLowerCase()) : null, 67 68 ); 68 69 } 69 70 70 71 if (isFollowing) { 71 72 return _ActionButton( 72 - label: 'Following', 73 + label: l10n.buttonFollowing, 73 74 onPressed: isOffline || onUnfollow == null ? null : () => _confirmUnfollow(context), 74 75 isLoading: isLoadingFollow, 75 76 isSecondary: true, 76 - tooltip: isOffline ? offlineActionMessage('change your follow state') : null, 77 + tooltip: isOffline ? l10n.formatOfflineReconnectAction('change your follow state') : null, 77 78 ); 78 79 } 79 80 80 81 return _ActionButton( 81 - label: 'Follow', 82 + label: l10n.buttonFollow, 82 83 onPressed: isOffline ? null : onFollow, 83 84 isLoading: isLoadingFollow, 84 - tooltip: isOffline ? offlineActionMessage('follow this account') : null, 85 + tooltip: isOffline ? l10n.formatOfflineReconnectAction('follow this account') : null, 85 86 ); 86 87 } 87 88 88 89 Widget _buildMoreButton(BuildContext context) { 90 + final l10n = context.l10n; 89 91 final List<PopupMenuEntry<void>> menuItems = []; 90 92 91 93 if (!isBlocked) { 92 94 if (isMuted) { 93 95 menuItems.add( 94 96 PopupMenuItem( 95 - child: const Row(children: [Icon(Icons.volume_up_outlined), SizedBox(width: 8), Text('Unmute')]), 97 + child: Row( 98 + children: [const Icon(Icons.volume_up_outlined), const SizedBox(width: 8), Text(l10n.buttonUnmute)], 99 + ), 96 100 onTap: () => _confirmUnmute(context), 97 101 ), 98 102 ); 99 103 } else { 100 104 menuItems.add( 101 105 PopupMenuItem( 102 - child: const Row(children: [Icon(Icons.volume_off_outlined), SizedBox(width: 8), Text('Mute')]), 106 + child: Row( 107 + children: [const Icon(Icons.volume_off_outlined), const SizedBox(width: 8), Text(l10n.buttonMute)], 108 + ), 103 109 onTap: () => _confirmMute(context), 104 110 ), 105 111 ); ··· 107 113 108 114 menuItems.add( 109 115 PopupMenuItem( 110 - child: const Row( 116 + child: Row( 111 117 children: [ 112 - Icon(Icons.block_outlined, color: Colors.red), 113 - SizedBox(width: 8), 114 - Text('Block', style: TextStyle(color: Colors.red)), 118 + const Icon(Icons.block_outlined, color: Colors.red), 119 + const SizedBox(width: 8), 120 + Text(l10n.buttonBlock, style: const TextStyle(color: Colors.red)), 115 121 ], 116 122 ), 117 123 onTap: () => _confirmBlock(context), ··· 123 129 menuItems.add( 124 130 PopupMenuItem( 125 131 onTap: onAddToList, 126 - child: const Row(children: [Icon(Icons.playlist_add_outlined), SizedBox(width: 8), Text('Add to list')]), 132 + child: Row( 133 + children: [const Icon(Icons.playlist_add_outlined), const SizedBox(width: 8), Text(l10n.labelAddToList)], 134 + ), 127 135 ), 128 136 ); 129 137 } ··· 132 140 const PopupMenuDivider(), 133 141 PopupMenuItem( 134 142 onTap: onMore, 135 - child: const Row( 143 + child: Row( 136 144 children: [ 137 - Icon(Icons.report_outlined, color: Colors.orange), 138 - SizedBox(width: 8), 139 - Text('Report', style: TextStyle(color: Colors.orange)), 145 + const Icon(Icons.report_outlined, color: Colors.orange), 146 + const SizedBox(width: 8), 147 + Text(l10n.labelReport, style: const TextStyle(color: Colors.orange)), 140 148 ], 141 149 ), 142 150 ), ··· 148 156 itemBuilder: (_) => menuItems, 149 157 ); 150 158 if (isOffline) { 151 - button = Tooltip(message: offlineActionMessage('manage this profile'), child: button); 159 + button = Tooltip(message: l10n.formatOfflineReconnectAction('manage this profile'), child: button); 152 160 } 153 161 return button; 154 162 } ··· 157 165 HapticHelper.mediumImpact(); 158 166 await showConfirmationDialog( 159 167 context: context, 160 - title: const Text('Unfollow?'), 161 - content: const Text('You will no longer see their posts in your feed.'), 162 - confirmLabel: 'Unfollow', 168 + title: Text(context.l10n.dialogUnfollowAccountTitle), 169 + content: Text(context.l10n.dialogUnfollowAccountContent), 170 + confirmLabel: context.l10n.buttonUnfollow, 163 171 onConfirmed: onUnfollow, 164 172 ); 165 173 } ··· 168 176 HapticHelper.mediumImpact(); 169 177 await showConfirmationDialog( 170 178 context: context, 171 - title: const Text('Mute Account?'), 172 - content: const Text('You will no longer see their posts or receive notifications from them.'), 173 - confirmLabel: 'Mute', 179 + title: Text(context.l10n.dialogMuteAccountTitle), 180 + content: Text(context.l10n.dialogMuteAccountContent), 181 + confirmLabel: context.l10n.buttonMute, 174 182 onConfirmed: onMute, 175 183 ); 176 184 } ··· 179 187 HapticHelper.mediumImpact(); 180 188 await showConfirmationDialog( 181 189 context: context, 182 - title: const Text('Unmute Account?'), 183 - content: const Text('You will see their posts and receive notifications again.'), 184 - confirmLabel: 'Unmute', 190 + title: Text(context.l10n.dialogUnmuteAccountTitle), 191 + content: Text(context.l10n.dialogUnmuteAccountContent), 192 + confirmLabel: context.l10n.buttonUnmute, 185 193 onConfirmed: onUnmute, 186 194 ); 187 195 } ··· 194 202 children: [ 195 203 Icon(Icons.block, color: context.colorScheme.error), 196 204 const SizedBox(width: 8), 197 - const Text('Block Account?'), 205 + Text(context.l10n.dialogBlockAccountTitle), 198 206 ], 199 207 ), 200 - content: const Text( 201 - 'They will not be able to see your posts or interact with you. They will not be notified that you blocked them.', 202 - ), 203 - confirmLabel: 'Block', 208 + content: Text(context.l10n.dialogBlockAccountContent), 209 + confirmLabel: context.l10n.buttonBlock, 204 210 confirmDestructive: true, 205 211 onConfirmed: onBlock, 206 212 ); ··· 210 216 HapticHelper.mediumImpact(); 211 217 await showConfirmationDialog( 212 218 context: context, 213 - title: const Text('Unblock Account?'), 214 - content: const Text('They will be able to see your posts and interact with you again.'), 215 - confirmLabel: 'Unblock', 219 + title: Text(context.l10n.dialogUnblockAccountTitle), 220 + content: Text(context.l10n.dialogUnblockAccountContent), 221 + confirmLabel: context.l10n.buttonUnblock, 216 222 onConfirmed: onUnblock, 217 223 ); 218 224 }
+5 -4
lib/features/profile/presentation/widgets/profile_liked_posts_pane.dart
··· 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter_bloc/flutter_bloc.dart'; 4 4 import 'package:go_router/go_router.dart'; 5 + import 'package:lazurite/core/l10n/l10n.dart'; 5 6 import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 6 7 import 'package:lazurite/features/feed/presentation/widgets/post_card_with_actions.dart'; 7 8 import 'package:lazurite/features/profile/data/profile_repository.dart'; ··· 103 104 children: [ 104 105 Text(_error!), 105 106 const SizedBox(height: 12), 106 - FilledButton(onPressed: _loadInitial, child: const Text('Retry')), 107 + FilledButton(onPressed: _loadInitial, child: Text(context.l10n.buttonRetry)), 107 108 ], 108 109 ), 109 110 ); 110 111 } 111 112 112 113 if (_entries.isEmpty) { 113 - return const Center(child: Text('No liked posts yet')); 114 + return Center(child: Text(context.l10n.messageNoLikedPostsYet)); 114 115 } 115 116 116 117 final accountDid = context.read<AuthBloc>().state.tokens?.did ?? ''; ··· 165 166 margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 166 167 child: ListTile( 167 168 leading: const Icon(Icons.hide_source_outlined), 168 - title: const Text('Unavailable liked post'), 169 + title: Text(context.l10n.labelUnavailableLikedPost), 169 170 subtitle: Text(reason), 170 171 trailing: IconButton( 171 172 icon: const Icon(Icons.open_in_new), 172 173 onPressed: () => context.push('/post?uri=${Uri.encodeQueryComponent(subjectUri)}'), 173 - tooltip: 'Open', 174 + tooltip: context.l10n.buttonOpen, 174 175 ), 175 176 ), 176 177 );
+4 -3
lib/features/profile/presentation/widgets/profile_starter_packs_pane.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:flutter_bloc/flutter_bloc.dart'; 3 3 import 'package:go_router/go_router.dart'; 4 + import 'package:lazurite/core/l10n/l10n.dart'; 4 5 import 'package:lazurite/features/starter_packs/cubit/actor_starter_packs_cubit.dart'; 5 6 import 'package:lazurite/features/starter_packs/data/starter_pack_repository.dart'; 6 7 import 'package:lazurite/features/starter_packs/presentation/widgets/starter_pack_card.dart'; ··· 53 54 child: Column( 54 55 mainAxisSize: MainAxisSize.min, 55 56 children: [ 56 - Text(state.errorMessage ?? 'Failed to load starter packs'), 57 + Text(state.errorMessage ?? context.l10n.errorFailedToLoadStarterPacks), 57 58 const SizedBox(height: 12), 58 59 FilledButton( 59 60 onPressed: () => _cubit.load(actor: widget.actor), 60 - child: const Text('Retry'), 61 + child: Text(context.l10n.buttonRetry), 61 62 ), 62 63 ], 63 64 ), ··· 65 66 } 66 67 67 68 if (state.starterPacks.isEmpty) { 68 - return const Center(child: Text('No starter packs yet')); 69 + return Center(child: Text(context.l10n.messageNoStarterPacksYet)); 69 70 } 70 71 71 72 return RefreshIndicator(
+61 -56
lib/features/profile/presentation/widgets/report_dialog.dart
··· 2 2 import 'package:atproto_core/atproto_core.dart'; 3 3 import 'package:flutter/material.dart'; 4 4 import 'package:flutter_bloc/flutter_bloc.dart'; 5 + import 'package:lazurite/core/l10n/l10n.dart'; 6 + import 'package:lazurite/core/theme/theme_extensions.dart'; 5 7 import 'package:lazurite/features/profile/cubit/profile_action_cubit.dart'; 6 8 import 'package:lazurite/shared/presentation/helpers/haptic_helper.dart'; 7 - import 'package:lazurite/core/theme/theme_extensions.dart'; 8 9 9 10 enum _ReportType { post, actor } 10 11 ··· 34 35 ReasonType? _selectedReason; 35 36 bool _isSubmitting = false; 36 37 37 - static const _reasonOptions = [ 38 - ( 39 - type: ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonSpam), 40 - label: 'Spam', 41 - description: 'Spam or unsolicited content', 42 - ), 43 - ( 44 - type: ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonViolation), 45 - label: 'Violation', 46 - description: 'Violates community guidelines', 47 - ), 48 - ( 49 - type: ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonMisleading), 50 - label: 'Misleading', 51 - description: 'Misleading or deceptive content', 52 - ), 53 - ( 54 - type: ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonSexual), 55 - label: 'Sexual Content', 56 - description: 'Unwanted sexual content', 57 - ), 58 - ( 59 - type: ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonRude), 60 - label: 'Harassment', 61 - description: 'Harassment or rude behaviour', 62 - ), 63 - ( 64 - type: ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonOther), 65 - label: 'Other', 66 - description: 'Other reason (requires explanation)', 67 - ), 68 - ]; 69 - 70 38 @override 71 39 void dispose() { 72 40 _reasonController.dispose(); ··· 75 43 76 44 @override 77 45 Widget build(BuildContext context) { 78 - final title = widget._type == _ReportType.post ? 'Report Post' : 'Report Account'; 46 + final l10n = context.l10n; 47 + final title = widget._type == _ReportType.post ? l10n.labelReportPost : l10n.labelReportAccount; 79 48 final target = widget.authorHandle; 80 49 81 50 return AlertDialog( 82 - title: Text('$title by @$target'), 51 + title: Text(l10n.formatProfileReportTitle(title, target)), 83 52 content: SizedBox( 84 53 width: double.maxFinite, 85 54 child: Column( 86 55 mainAxisSize: MainAxisSize.min, 87 56 crossAxisAlignment: CrossAxisAlignment.start, 88 57 children: [ 89 - Text('Reason', style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)), 58 + Text(l10n.labelReportReason, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)), 90 59 const SizedBox(height: 8), 91 - ..._reasonOptions.map((option) => _buildReasonOption(option)), 60 + ..._reasonOptions(context).map((option) => _buildReasonOption(option)), 92 61 if (_requiresExplanation) ...[ 93 62 const SizedBox(height: 16), 94 63 Text( 95 - 'Explanation (required)', 64 + l10n.labelReportReasonExplanationRequired, 96 65 style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), 97 66 ), 98 67 const SizedBox(height: 8), ··· 100 69 controller: _reasonController, 101 70 maxLines: 3, 102 71 maxLength: 2000, 103 - decoration: const InputDecoration( 104 - hintText: 'Please explain why you are reporting this...', 105 - border: OutlineInputBorder(), 72 + decoration: InputDecoration( 73 + hintText: l10n.messageReportExplanationHint, 74 + border: const OutlineInputBorder(), 106 75 ), 107 76 ), 108 77 ], ··· 110 79 ), 111 80 ), 112 81 actions: [ 113 - TextButton(onPressed: _isSubmitting ? null : () => Navigator.pop(context), child: const Text('Cancel')), 82 + TextButton(onPressed: _isSubmitting ? null : () => Navigator.pop(context), child: Text(l10n.buttonCancel)), 114 83 FilledButton( 115 84 onPressed: _canSubmit ? _submit : null, 116 85 child: _isSubmitting 117 86 ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) 118 - : const Text('Submit Report'), 87 + : Text(l10n.buttonSubmitReport), 119 88 ), 120 89 ], 121 90 ); 122 91 } 123 92 93 + List<({ReasonType type, String label, String description})> _reasonOptions(BuildContext context) { 94 + final l10n = context.l10n; 95 + return [ 96 + ( 97 + type: const ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonSpam), 98 + label: l10n.labelReportReasonSpam, 99 + description: l10n.messageReportReasonSpamDescription, 100 + ), 101 + ( 102 + type: const ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonViolation), 103 + label: l10n.labelReportReasonViolation, 104 + description: l10n.messageReportReasonViolationDescription, 105 + ), 106 + ( 107 + type: const ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonMisleading), 108 + label: l10n.labelReportReasonMisleading, 109 + description: l10n.messageReportReasonMisleadingDescription, 110 + ), 111 + ( 112 + type: const ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonSexual), 113 + label: l10n.labelReportReasonSexualContent, 114 + description: l10n.messageReportReasonSexualContentDescription, 115 + ), 116 + ( 117 + type: const ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonRude), 118 + label: l10n.labelReportReasonHarassment, 119 + description: l10n.messageReportReasonHarassmentDescription, 120 + ), 121 + ( 122 + type: const ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonOther), 123 + label: l10n.labelReportReasonOther, 124 + description: l10n.messageReportReasonOtherDescription, 125 + ), 126 + ]; 127 + } 128 + 124 129 Widget _buildReasonOption(({ReasonType type, String label, String description}) option) { 125 130 final isSelected = _selectedReason == option.type; 126 131 final theme = Theme.of(context); ··· 225 230 showDialog<void>( 226 231 context: context, 227 232 builder: (context) => AlertDialog( 228 - title: const Row( 233 + title: Row( 229 234 children: [ 230 - Icon(Icons.check_circle, color: Colors.green), 231 - SizedBox(width: 8), 232 - Text('Report Submitted'), 235 + const Icon(Icons.check_circle, color: Colors.green), 236 + const SizedBox(width: 8), 237 + Text(context.l10n.labelReportSubmitted), 233 238 ], 234 239 ), 235 - content: Text('Thank you. Your report (ID: $reportId) has been submitted.'), 236 - actions: [FilledButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], 240 + content: Text(context.l10n.formatReportSubmitted(reportId)), 241 + actions: [FilledButton(onPressed: () => Navigator.pop(context), child: Text(context.l10n.buttonOk))], 237 242 ), 238 243 ); 239 244 } ··· 242 247 showDialog<void>( 243 248 context: context, 244 249 builder: (context) => AlertDialog( 245 - title: const Row( 250 + title: Row( 246 251 children: [ 247 - Icon(Icons.error_outline, color: Colors.red), 248 - SizedBox(width: 8), 249 - Text('Report Failed'), 252 + const Icon(Icons.error_outline, color: Colors.red), 253 + const SizedBox(width: 8), 254 + Text(context.l10n.errorReportFailedTitle), 250 255 ], 251 256 ), 252 - content: const Text('Unable to submit your report. Please try again later.'), 253 - actions: [FilledButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], 257 + content: Text(context.l10n.errorReportFailed), 258 + actions: [FilledButton(onPressed: () => Navigator.pop(context), child: Text(context.l10n.buttonOk))], 254 259 ), 255 260 ); 256 261 }
+11 -7
lib/features/profile/presentation/widgets/suggested_follows_list.dart
··· 1 1 import 'package:bluesky/app_bsky_actor_defs.dart'; 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter_bloc/flutter_bloc.dart'; 4 + import 'package:lazurite/core/l10n/l10n.dart'; 4 5 import 'package:lazurite/features/profile/cubit/profile_action_cubit.dart'; 5 6 import 'package:lazurite/features/profile/cubit/suggested_follows_cubit.dart'; 6 7 import 'package:lazurite/features/profile/data/profile_action_repository.dart'; ··· 15 16 required this.actor, 16 17 this.scrollController, 17 18 this.onProfileTap, 18 - this.emptyMessage = 'No suggestions found', 19 + this.emptyMessage, 19 20 this.padding = EdgeInsets.zero, 20 21 }); 21 22 22 23 final String actor; 23 24 final ScrollController? scrollController; 24 25 final ValueChanged<ProfileView>? onProfileTap; 25 - final String emptyMessage; 26 + final String? emptyMessage; 26 27 final EdgeInsetsGeometry padding; 27 28 28 29 @override ··· 35 36 36 37 if (state.hasError) { 37 38 return ErrorState( 38 - title: 'Failed to load suggestions', 39 - message: state.errorMessage ?? 'Unknown error', 39 + title: context.l10n.errorFailedToLoadSuggestions, 40 + message: state.errorMessage ?? context.l10n.errorUnknown, 40 41 onRetry: () => context.read<SuggestedFollowsCubit>().load(actor), 41 42 ); 42 43 } 43 44 44 45 if (state.isEmpty) { 45 - return EmptyState(message: emptyMessage, icon: Icons.person_search_outlined); 46 + return EmptyState( 47 + message: emptyMessage ?? context.l10n.messageNoSuggestionsFound, 48 + icon: Icons.person_search_outlined, 49 + ); 46 50 } 47 51 48 52 return ListView.builder( ··· 162 166 } 163 167 164 168 if (isFollowing) { 165 - return OutlinedButton(onPressed: onPressed, child: const Text('Following')); 169 + return OutlinedButton(onPressed: onPressed, child: Text(context.l10n.buttonFollowing)); 166 170 } 167 171 168 - return FilledButton(onPressed: onPressed, child: const Text('Follow')); 172 + return FilledButton(onPressed: onPressed, child: Text(context.l10n.buttonFollow)); 169 173 } 170 174 }
+2 -1
lib/features/profile/presentation/widgets/suggested_follows_sheet.dart
··· 1 1 import 'package:flutter/material.dart'; 2 + import 'package:lazurite/core/l10n/l10n.dart'; 2 3 import 'package:lazurite/features/profile/presentation/widgets/suggested_follows_list.dart'; 3 4 import 'package:lazurite/shared/presentation/helpers/navigation_helpers.dart'; 4 5 import 'package:lazurite/core/theme/theme_extensions.dart'; ··· 20 21 Padding( 21 22 padding: const EdgeInsets.symmetric(vertical: 12), 22 23 child: Text( 23 - 'Suggested Follows', 24 + context.l10n.labelSuggestedFollows, 24 25 style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), 25 26 ), 26 27 ),
+6 -5
lib/features/starter_packs/presentation/actor_starter_packs_screen.dart
··· 2 2 import 'package:flutter_animate/flutter_animate.dart'; 3 3 import 'package:flutter_bloc/flutter_bloc.dart'; 4 4 import 'package:go_router/go_router.dart'; 5 + import 'package:lazurite/core/l10n/l10n.dart'; 5 6 import 'package:lazurite/core/theme/animation_tokens.dart'; 6 7 import 'package:lazurite/core/theme/animation_utils.dart'; 7 8 import 'package:lazurite/features/starter_packs/cubit/actor_starter_packs_cubit.dart'; ··· 40 41 final isOwnProfile = currentUserDid != null && currentUserDid == actor; 41 42 42 43 return Scaffold( 43 - appBar: AppBar(title: const Text('Starter Packs')), 44 + appBar: AppBar(title: Text(context.l10n.labelStarterPacks)), 44 45 floatingActionButton: isOwnProfile 45 46 ? FloatingActionButton( 46 47 onPressed: () => context.push('/create-starter-pack'), 47 - tooltip: 'Create starter pack', 48 + tooltip: context.l10n.labelCreateStarterPack, 48 49 child: const Icon(Icons.add), 49 50 ).animateIfAllowed( 50 51 context, ··· 65 66 child: Column( 66 67 mainAxisSize: MainAxisSize.min, 67 68 children: [ 68 - Text(state.errorMessage ?? 'Failed to load starter packs'), 69 + Text(state.errorMessage ?? context.l10n.errorFailedToLoadStarterPacks), 69 70 const SizedBox(height: 12), 70 71 FilledButton( 71 72 onPressed: () => context.read<ActorStarterPacksCubit>().load(actor: actor), 72 - child: const Text('Retry'), 73 + child: Text(context.l10n.buttonRetry), 73 74 ), 74 75 ], 75 76 ), ··· 77 78 } 78 79 79 80 if (state.starterPacks.isEmpty) { 80 - return const Center(child: Text('No starter packs yet')); 81 + return Center(child: Text(context.l10n.messageNoStarterPacksYet)); 81 82 } 82 83 83 84 return AnimatedRefreshIndicator(
+23 -16
lib/features/starter_packs/presentation/create_edit_starter_pack_screen.dart
··· 4 4 import 'package:flutter/material.dart'; 5 5 import 'package:flutter_bloc/flutter_bloc.dart'; 6 6 import 'package:go_router/go_router.dart'; 7 + import 'package:lazurite/core/l10n/l10n.dart'; 8 + import 'package:lazurite/core/theme/theme_extensions.dart'; 7 9 import 'package:lazurite/features/starter_packs/bloc/starter_pack_bloc.dart'; 8 10 import 'package:lazurite/features/starter_packs/data/starter_pack_repository.dart'; 9 11 import 'package:lazurite/features/typeahead/data/typeahead_repository.dart'; 10 - import 'package:lazurite/core/theme/theme_extensions.dart'; 11 12 import 'package:lazurite/shared/presentation/widgets/profile_avatar.dart'; 12 13 13 14 /// Full-screen form for creating a new starter pack. ··· 127 128 } else if (state.status == StarterPackStatus.error) { 128 129 ScaffoldMessenger.of(context).showSnackBar( 129 130 SnackBar( 130 - content: Text(state.errorMessage ?? 'Failed to create starter pack'), 131 + content: Text(state.errorMessage ?? context.l10n.errorFailedToCreateStarterPack), 131 132 behavior: SnackBarBehavior.floating, 132 133 ), 133 134 ); ··· 139 140 140 141 return Scaffold( 141 142 appBar: AppBar( 142 - title: const Text('New Starter Pack'), 143 + title: Text(context.l10n.labelNewStarterPack), 143 144 actions: [ 144 145 if (isCreating) 145 146 const Padding( ··· 147 148 child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)), 148 149 ) 149 150 else 150 - TextButton(onPressed: _canSave ? _save : null, child: const Text('Create')), 151 + TextButton(onPressed: _canSave ? _save : null, child: Text(context.l10n.buttonCreate)), 151 152 ], 152 153 ), 153 154 body: ListView( ··· 155 156 children: [ 156 157 TextField( 157 158 controller: _nameController, 158 - decoration: const InputDecoration( 159 - labelText: 'Name', 160 - border: OutlineInputBorder(), 161 - helperText: 'Required, max 50 characters', 159 + decoration: InputDecoration( 160 + labelText: context.l10n.labelName, 161 + border: const OutlineInputBorder(), 162 + helperText: context.l10n.formatValidationRequiredMaxCharacters(50), 162 163 ), 163 164 maxLength: 50, 164 165 textCapitalization: TextCapitalization.sentences, ··· 167 168 const SizedBox(height: 16), 168 169 TextField( 169 170 controller: _descController, 170 - decoration: const InputDecoration(labelText: 'Description (optional)', border: OutlineInputBorder()), 171 + decoration: InputDecoration( 172 + labelText: context.l10n.labelDescriptionOptional, 173 + border: const OutlineInputBorder(), 174 + ), 171 175 maxLength: 300, 172 176 maxLines: 3, 173 177 textCapitalization: TextCapitalization.sentences, ··· 192 196 return Column( 193 197 crossAxisAlignment: CrossAxisAlignment.start, 194 198 children: [ 195 - Text('Members', style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), 199 + Text(context.l10n.labelMembers, style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), 196 200 const SizedBox(height: 8), 197 201 TextField( 198 202 controller: _searchController, 199 203 decoration: InputDecoration( 200 - hintText: 'Search for people to add', 204 + hintText: context.l10n.messageSearchPeopleToAddPlaceholder, 201 205 prefixIcon: const Icon(Icons.search), 202 206 suffixIcon: _isSearching 203 207 ? const Padding( ··· 278 282 children: [ 279 283 Row( 280 284 children: [ 281 - Text('Feeds', style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), 285 + Text(context.l10n.labelFeeds, style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), 282 286 const SizedBox(width: 8), 283 - Text('(up to 3)', style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant)), 287 + Text( 288 + context.l10n.labelUpToThree, 289 + style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), 290 + ), 284 291 ], 285 292 ), 286 293 const SizedBox(height: 8), ··· 308 315 OutlinedButton.icon( 309 316 onPressed: isCreating ? null : _showFeedPicker, 310 317 icon: const Icon(Icons.add), 311 - label: const Text('Add feed'), 318 + label: Text(context.l10n.buttonAddFeed), 312 319 ), 313 320 ], 314 321 ); ··· 340 347 final feeds = await widget.starterPackRepository.getSuggestedFeeds(limit: 50); 341 348 if (mounted) setState(() => _feeds = feeds); 342 349 } catch (e) { 343 - if (mounted) setState(() => _error = 'Failed to load feeds'); 350 + if (mounted) setState(() => _error = context.l10n.errorFailedToLoadFeeds); 344 351 } 345 352 } 346 353 ··· 360 367 padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), 361 368 child: Row( 362 369 children: [ 363 - Text('Select a feed', style: context.textTheme.titleMedium), 370 + Text(context.l10n.labelSelectFeed, style: context.textTheme.titleMedium), 364 371 const Spacer(), 365 372 IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)), 366 373 ],
+48 -31
lib/features/starter_packs/presentation/starter_pack_detail_screen.dart
··· 5 5 import 'package:flutter/material.dart' as material show ListView; 6 6 import 'package:flutter_bloc/flutter_bloc.dart'; 7 7 import 'package:go_router/go_router.dart'; 8 + import 'package:lazurite/core/l10n/l10n.dart'; 9 + import 'package:lazurite/core/theme/theme_extensions.dart'; 8 10 import 'package:lazurite/features/starter_packs/bloc/starter_pack_bloc.dart'; 9 11 import 'package:lazurite/features/starter_packs/data/starter_pack_repository.dart'; 10 12 import 'package:lazurite/shared/presentation/helpers/navigation_helpers.dart'; 11 13 import 'package:lazurite/shared/utils/format_utils.dart'; 12 - import 'package:lazurite/core/theme/theme_extensions.dart'; 13 14 14 15 class StarterPackDetailScreen extends StatelessWidget { 15 16 const StarterPackDetailScreen({super.key, required this.packUri}); ··· 50 51 if (state.followedCount != null) { 51 52 ScaffoldMessenger.of(context).showSnackBar( 52 53 SnackBar( 53 - content: Text('Followed ${state.followedCount} member${state.followedCount == 1 ? '' : 's'}'), 54 + content: Text(context.l10n.formatFollowedMemberCount(state.followedCount!)), 54 55 behavior: SnackBarBehavior.floating, 55 56 ), 56 57 ); ··· 75 76 floating: true, 76 77 pinned: true, 77 78 snap: true, 78 - title: Text(pack != null ? ((pack.record['name'] as String?) ?? 'Starter Pack') : 'Starter Pack'), 79 + title: Text( 80 + pack != null 81 + ? ((pack.record['name'] as String?) ?? context.l10n.labelStarterPack) 82 + : context.l10n.labelStarterPack, 83 + ), 79 84 actions: [ 80 85 if (state.isMutating) 81 86 const Padding( ··· 85 90 else if (isCreator && pack != null) 86 91 PopupMenuButton<_PackAction>( 87 92 onSelected: (action) => _handleAction(context, action, state, pack), 88 - itemBuilder: (_) => const [ 89 - PopupMenuItem(value: _PackAction.edit, child: Text('Edit')), 90 - PopupMenuItem(value: _PackAction.delete, child: Text('Delete')), 93 + itemBuilder: (_) => [ 94 + PopupMenuItem(value: _PackAction.edit, child: Text(context.l10n.buttonEdit)), 95 + PopupMenuItem(value: _PackAction.delete, child: Text(context.l10n.buttonDelete)), 91 96 ], 92 97 ), 93 98 ], ··· 102 107 child: Column( 103 108 mainAxisSize: MainAxisSize.min, 104 109 children: [ 105 - Text(state.errorMessage ?? 'Failed to load starter pack'), 110 + Text(state.errorMessage ?? context.l10n.errorFailedToLoadStarterPack), 106 111 const SizedBox(height: 12), 107 112 FilledButton( 108 113 onPressed: () => context.read<StarterPackBloc>().add( 109 114 StarterPackRequested(starterPackUri: state.packUri!), 110 115 ), 111 - child: const Text('Retry'), 116 + child: Text(context.l10n.buttonRetry), 112 117 ), 113 118 ], 114 119 ), ··· 167 172 final confirmed = await showDialog<bool>( 168 173 context: context, 169 174 builder: (_) => AlertDialog( 170 - title: const Text('Delete starter pack'), 171 - content: const Text( 172 - 'This will permanently delete this starter pack and its backing list. This cannot be undone.', 173 - ), 175 + title: Text(context.l10n.dialogDeleteStarterPackTitle), 176 + content: Text(context.l10n.dialogDeleteStarterPackContent), 174 177 actions: [ 175 - TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), 178 + TextButton(onPressed: () => Navigator.pop(context, false), child: Text(context.l10n.buttonCancel)), 176 179 FilledButton( 177 180 style: FilledButton.styleFrom(backgroundColor: context.colorScheme.error), 178 181 onPressed: () => Navigator.pop(context, true), 179 - child: const Text('Delete'), 182 + child: Text(context.l10n.buttonDelete), 180 183 ), 181 184 ], 182 185 ), ··· 258 261 final colorScheme = context.colorScheme; 259 262 260 263 return AlertDialog( 261 - title: const Text('Edit starter pack'), 264 + title: Text(context.l10n.labelEditStarterPack), 262 265 content: SizedBox( 263 266 width: double.maxFinite, 264 267 child: SingleChildScrollView( ··· 268 271 children: [ 269 272 TextField( 270 273 controller: _nameController, 271 - decoration: const InputDecoration(labelText: 'Name', border: OutlineInputBorder()), 274 + decoration: InputDecoration(labelText: context.l10n.labelName, border: const OutlineInputBorder()), 272 275 maxLength: 50, 273 276 textCapitalization: TextCapitalization.sentences, 274 277 ), 275 278 const SizedBox(height: 12), 276 279 TextField( 277 280 controller: _descController, 278 - decoration: const InputDecoration(labelText: 'Description (optional)', border: OutlineInputBorder()), 281 + decoration: InputDecoration( 282 + labelText: context.l10n.labelDescriptionOptional, 283 + border: const OutlineInputBorder(), 284 + ), 279 285 maxLength: 300, 280 286 maxLines: 3, 281 287 textCapitalization: TextCapitalization.sentences, 282 288 ), 283 289 const SizedBox(height: 12), 284 - Text('Feeds', style: context.textTheme.labelLarge), 290 + Text(context.l10n.labelFeeds, style: context.textTheme.labelLarge), 285 291 const SizedBox(height: 8), 286 292 for (final feed in _feeds) 287 293 ListTile( ··· 300 306 ), 301 307 ), 302 308 if (_feeds.length < 3) 303 - TextButton.icon(onPressed: _showFeedPicker, icon: const Icon(Icons.add), label: const Text('Add feed')), 309 + TextButton.icon( 310 + onPressed: _showFeedPicker, 311 + icon: const Icon(Icons.add), 312 + label: Text(context.l10n.buttonAddFeed), 313 + ), 304 314 ], 305 315 ), 306 316 ), 307 317 ), 308 318 actions: [ 309 - TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), 310 - FilledButton(onPressed: _nameController.text.trim().isEmpty ? null : _save, child: const Text('Save')), 319 + TextButton(onPressed: () => Navigator.pop(context), child: Text(context.l10n.buttonCancel)), 320 + FilledButton( 321 + onPressed: _nameController.text.trim().isEmpty ? null : _save, 322 + child: Text(context.l10n.buttonSave), 323 + ), 311 324 ], 312 325 ); 313 326 } ··· 338 351 final feeds = await widget.starterPackRepository.getSuggestedFeeds(limit: 50); 339 352 if (mounted) setState(() => _feeds = feeds); 340 353 } catch (e) { 341 - if (mounted) setState(() => _error = 'Failed to load feeds'); 354 + if (mounted) setState(() => _error = context.l10n.errorFailedToLoadFeeds); 342 355 } 343 356 } 344 357 ··· 358 371 padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), 359 372 child: Row( 360 373 children: [ 361 - Text('Select a feed', style: context.textTheme.titleMedium), 374 + Text(context.l10n.labelSelectFeed, style: context.textTheme.titleMedium), 362 375 const Spacer(), 363 376 IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)), 364 377 ], ··· 412 425 final colorScheme = context.colorScheme; 413 426 final textTheme = context.textTheme; 414 427 415 - final name = (pack.record['name'] as String?) ?? 'Starter Pack'; 428 + final name = (pack.record['name'] as String?) ?? context.l10n.labelStarterPack; 416 429 final description = pack.record['description'] as String?; 417 430 418 431 return Padding( ··· 466 479 Row( 467 480 children: [ 468 481 if (pack.joinedWeekCount != null) ...[ 469 - _buildStatChip(context, pack.joinedWeekCount!, 'joined this week'), 482 + _buildStatChip(context, pack.joinedWeekCount!, context.l10n.labelJoinedThisWeek), 470 483 const SizedBox(width: 12), 471 484 ], 472 - if (pack.joinedAllTimeCount != null) _buildStatChip(context, pack.joinedAllTimeCount!, 'total joined'), 485 + if (pack.joinedAllTimeCount != null) 486 + _buildStatChip(context, pack.joinedAllTimeCount!, context.l10n.labelTotalJoined), 473 487 ], 474 488 ), 475 489 const SizedBox(height: 16), ··· 516 530 children: [ 517 531 Row( 518 532 children: [ 519 - Text('Members', style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)), 533 + Text(context.l10n.labelMembers, style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)), 520 534 if (refListUri != null) ...[ 521 535 const Spacer(), 522 536 TextButton( 523 537 onPressed: () => context.push('/list?uri=${Uri.encodeComponent(refListUri.toString())}'), 524 - child: const Text('See all'), 538 + child: Text(context.l10n.buttonSeeAll), 525 539 ), 526 540 ], 527 541 ], 528 542 ), 529 543 const SizedBox(height: 8), 530 544 if (sample.isEmpty) 531 - Text('No members', style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)) 545 + Text( 546 + context.l10n.messageNoMembers, 547 + style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), 548 + ) 532 549 else 533 550 SizedBox( 534 551 height: 72, ··· 586 603 icon: state.isFollowingAll 587 604 ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) 588 605 : const Icon(Icons.group_add_outlined), 589 - label: Text(state.isFollowingAll ? 'Following…' : 'Follow all'), 606 + label: Text(state.isFollowingAll ? context.l10n.buttonFollowingInProgress : context.l10n.buttonFollowAll), 590 607 ), 591 608 ), 592 609 ], ··· 602 619 return Column( 603 620 crossAxisAlignment: CrossAxisAlignment.start, 604 621 children: [ 605 - Text('Recommended Feeds', style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)), 622 + Text(context.l10n.labelRecommendedFeeds, style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)), 606 623 const SizedBox(height: 8), 607 624 for (final feed in feeds.take(3)) 608 625 ListTile(
+7 -6
lib/features/starter_packs/presentation/widgets/starter_pack_card.dart
··· 1 1 import 'package:bluesky/app_bsky_graph_defs.dart'; 2 2 import 'package:flutter/material.dart'; 3 + import 'package:lazurite/core/l10n/l10n.dart'; 4 + import 'package:lazurite/core/theme/theme_extensions.dart'; 3 5 import 'package:lazurite/shared/presentation/widgets/profile_avatar.dart'; 4 6 import 'package:lazurite/shared/utils/format_utils.dart'; 5 - import 'package:lazurite/core/theme/theme_extensions.dart'; 6 7 7 8 class StarterPackCard extends StatelessWidget { 8 9 const StarterPackCard({super.key, required this.pack, this.onTap}); ··· 15 16 final colorScheme = context.colorScheme; 16 17 final textTheme = context.textTheme; 17 18 18 - final name = (pack.record['name'] as String?) ?? 'Starter Pack'; 19 + final name = (pack.record['name'] as String?) ?? context.l10n.labelStarterPack; 19 20 final memberCount = pack.listItemCount; 20 21 final joinedWeek = pack.joinedWeekCount; 21 22 final joinedAll = pack.joinedAllTimeCount; ··· 50 51 overflow: TextOverflow.ellipsis, 51 52 ), 52 53 Text( 53 - 'by @${pack.creator.handle}', 54 + context.l10n.formatListByHandle(pack.creator.handle), 54 55 style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), 55 56 ), 56 57 ], ··· 63 64 Wrap( 64 65 spacing: 16, 65 66 children: [ 66 - if (memberCount != null) _buildStat(context, memberCount, 'members'), 67 - if (joinedWeek != null) _buildStat(context, joinedWeek, 'joined this week'), 68 - if (joinedAll != null) _buildStat(context, joinedAll, 'joined total'), 67 + if (memberCount != null) _buildStat(context, memberCount, context.l10n.labelMembers.toLowerCase()), 68 + if (joinedWeek != null) _buildStat(context, joinedWeek, context.l10n.labelJoinedThisWeek), 69 + if (joinedAll != null) _buildStat(context, joinedAll, context.l10n.labelJoinedTotal), 69 70 ], 70 71 ), 71 72 ],
+1 -1
test/features/feed/presentation/post_interactions_sheet_test.dart
··· 157 157 await tester.pump(); 158 158 await tester.pump(); 159 159 160 - await tester.tap(find.text('1 Reposts')); 160 + await tester.tap(find.text('1 Repost')); 161 161 await tester.pump(); 162 162 await tester.pump(); 163 163