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

Configure Feed

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

refactor: add localizations

+1284 -107
+480
lib/src/core/l10n/app_localizations.dart
··· 148 148 /// **'Close'** 149 149 String get buttonClose; 150 150 151 + /// Add button text 152 + /// 153 + /// In en, this message translates to: 154 + /// **'Add'** 155 + String get buttonAdd; 156 + 157 + /// Remove button text 158 + /// 159 + /// In en, this message translates to: 160 + /// **'Remove'** 161 + String get buttonRemove; 162 + 163 + /// Retry button text 164 + /// 165 + /// In en, this message translates to: 166 + /// **'Retry'** 167 + String get buttonRetry; 168 + 169 + /// Try again button text 170 + /// 171 + /// In en, this message translates to: 172 + /// **'Try again'** 173 + String get buttonTryAgain; 174 + 175 + /// Go back button text 176 + /// 177 + /// In en, this message translates to: 178 + /// **'Go Back'** 179 + String get buttonGoBack; 180 + 181 + /// Allow access button for permissions 182 + /// 183 + /// In en, this message translates to: 184 + /// **'Allow Access'** 185 + String get buttonAllowAccess; 186 + 187 + /// Open settings button 188 + /// 189 + /// In en, this message translates to: 190 + /// **'Open Settings'** 191 + String get buttonOpenSettings; 192 + 193 + /// Submit button text 194 + /// 195 + /// In en, this message translates to: 196 + /// **'Submit'** 197 + String get buttonSubmit; 198 + 151 199 /// Error message for required fields 152 200 /// 153 201 /// In en, this message translates to: ··· 171 219 /// In en, this message translates to: 172 220 /// **'Search...'** 173 221 String get searchPlaceholder; 222 + 223 + /// Settings page title 224 + /// 225 + /// In en, this message translates to: 226 + /// **'Settings'** 227 + String get pageTitleSettings; 228 + 229 + /// Story manager page title 230 + /// 231 + /// In en, this message translates to: 232 + /// **'Story Manager'** 233 + String get pageTitleStoryManager; 234 + 235 + /// Sound page title 236 + /// 237 + /// In en, this message translates to: 238 + /// **'Sound'** 239 + String get pageTitleSound; 240 + 241 + /// Your feeds page title 242 + /// 243 + /// In en, this message translates to: 244 + /// **'Your Feeds'** 245 + String get pageTitleYourFeeds; 246 + 247 + /// Blocked users page title 248 + /// 249 + /// In en, this message translates to: 250 + /// **'Blocked Users'** 251 + String get pageTitleBlockedUsers; 252 + 253 + /// Edit profile page title 254 + /// 255 + /// In en, this message translates to: 256 + /// **'Edit Profile'** 257 + String get pageTitleEditProfile; 258 + 259 + /// Complete profile page title 260 + /// 261 + /// In en, this message translates to: 262 + /// **'Complete your profile'** 263 + String get pageTitleCompleteProfile; 264 + 265 + /// Labeler settings page title 266 + /// 267 + /// In en, this message translates to: 268 + /// **'Labeler Settings'** 269 + String get pageTitleLabelerSettings; 270 + 271 + /// Labelers page title 272 + /// 273 + /// In en, this message translates to: 274 + /// **'Labelers'** 275 + String get pageTitleLabelers; 276 + 277 + /// Result page title 278 + /// 279 + /// In en, this message translates to: 280 + /// **'Result'** 281 + String get pageTitleResult; 282 + 283 + /// Delete story dialog title 284 + /// 285 + /// In en, this message translates to: 286 + /// **'Delete Story'** 287 + String get dialogDeleteStory; 288 + 289 + /// Delete story confirmation message 290 + /// 291 + /// In en, this message translates to: 292 + /// **'Are you sure you want to delete this story?'** 293 + String get dialogDeleteStoryConfirm; 294 + 295 + /// Remove labeler dialog title 296 + /// 297 + /// In en, this message translates to: 298 + /// **'Remove Labeler'** 299 + String get dialogRemoveLabeler; 300 + 301 + /// Remove labeler confirmation message 302 + /// 303 + /// In en, this message translates to: 304 + /// **'Are you sure you want to remove this labeler?'** 305 + String get dialogRemoveLabelerConfirm; 306 + 307 + /// Remove feed dialog title 308 + /// 309 + /// In en, this message translates to: 310 + /// **'Remove Feed'** 311 + String get dialogRemoveFeed; 312 + 313 + /// Delete post dialog title 314 + /// 315 + /// In en, this message translates to: 316 + /// **'Delete Post'** 317 + String get dialogDeletePost; 318 + 319 + /// Delete comment dialog title 320 + /// 321 + /// In en, this message translates to: 322 + /// **'Delete Comment'** 323 + String get dialogDeleteComment; 324 + 325 + /// Empty state for no users 326 + /// 327 + /// In en, this message translates to: 328 + /// **'No users to display.'** 329 + String get emptyNoUsers; 330 + 331 + /// Empty state for no blocked users 332 + /// 333 + /// In en, this message translates to: 334 + /// **'No blocked users.'** 335 + String get emptyNoBlockedUsers; 336 + 337 + /// Empty state for no stories 338 + /// 339 + /// In en, this message translates to: 340 + /// **'No stories'** 341 + String get emptyNoStories; 342 + 343 + /// Empty state for no comments 344 + /// 345 + /// In en, this message translates to: 346 + /// **'No comments yet.'** 347 + String get emptyNoComments; 348 + 349 + /// Empty state for no crosspost comments 350 + /// 351 + /// In en, this message translates to: 352 + /// **'No crosspost comments yet.'** 353 + String get emptyNoCrosspostComments; 354 + 355 + /// Empty state for no labelers 356 + /// 357 + /// In en, this message translates to: 358 + /// **'No Labelers'** 359 + String get emptyNoLabelers; 360 + 361 + /// Description for empty labelers state 362 + /// 363 + /// In en, this message translates to: 364 + /// **'Add labelers to customize content moderation'** 365 + String get emptyNoLabelersDescription; 366 + 367 + /// Empty state for discover content 368 + /// 369 + /// In en, this message translates to: 370 + /// **'Discover new content'** 371 + String get emptyDiscoverContent; 372 + 373 + /// Empty state for no media 374 + /// 375 + /// In en, this message translates to: 376 + /// **'No photos or videos found...'** 377 + String get emptyNoMedia; 378 + 379 + /// Generic error message 380 + /// 381 + /// In en, this message translates to: 382 + /// **'An error occurred'** 383 + String get errorGeneric; 384 + 385 + /// Error loading feed message 386 + /// 387 + /// In en, this message translates to: 388 + /// **'Error loading feed'** 389 + String get errorLoadingFeed; 390 + 391 + /// Error loading sound message 392 + /// 393 + /// In en, this message translates to: 394 + /// **'Error loading sound'** 395 + String get errorLoadingSound; 396 + 397 + /// Error loading reposts message 398 + /// 399 + /// In en, this message translates to: 400 + /// **'Error loading reposts'** 401 + String get errorLoadingReposts; 402 + 403 + /// Error loading likes message 404 + /// 405 + /// In en, this message translates to: 406 + /// **'Error loading likes'** 407 + String get errorLoadingLikes; 408 + 409 + /// Error loading posts message 410 + /// 411 + /// In en, this message translates to: 412 + /// **'Error loading posts'** 413 + String get errorLoadingPosts; 414 + 415 + /// Search placeholder for users and posts 416 + /// 417 + /// In en, this message translates to: 418 + /// **'Search users, posts...'** 419 + String get hintSearchUsersPosts; 420 + 421 + /// Search placeholder for users 422 + /// 423 + /// In en, this message translates to: 424 + /// **'Search users'** 425 + String get hintSearchUsers; 426 + 427 + /// Message input placeholder 428 + /// 429 + /// In en, this message translates to: 430 + /// **'Message...'** 431 + String get hintMessage; 432 + 433 + /// Type message input placeholder 434 + /// 435 + /// In en, this message translates to: 436 + /// **'Type a message...'** 437 + String get hintTypeMessage; 438 + 439 + /// Add description placeholder 440 + /// 441 + /// In en, this message translates to: 442 + /// **'Add a description... (optional)'** 443 + String get hintAddDescription; 444 + 445 + /// Add alt text placeholder 446 + /// 447 + /// In en, this message translates to: 448 + /// **'Add alt text'** 449 + String get hintAddAltText; 450 + 451 + /// Display name input label 452 + /// 453 + /// In en, this message translates to: 454 + /// **'Display Name'** 455 + String get hintDisplayName; 456 + 457 + /// Bio input label 458 + /// 459 + /// In en, this message translates to: 460 + /// **'Bio'** 461 + String get hintBio; 462 + 463 + /// DID or handle input label 464 + /// 465 + /// In en, this message translates to: 466 + /// **'DID or Handle'** 467 + String get hintDidOrHandle; 468 + 469 + /// DID or handle input hint 470 + /// 471 + /// In en, this message translates to: 472 + /// **'did:plc:... or @handle.bsky.social'** 473 + String get hintDidOrHandleExample; 474 + 475 + /// Additional details input placeholder 476 + /// 477 + /// In en, this message translates to: 478 + /// **'Additional details (optional)'** 479 + String get hintAdditionalDetails; 480 + 481 + /// Image description input label 482 + /// 483 + /// In en, this message translates to: 484 + /// **'Image Description'** 485 + String get hintImageDescription; 486 + 487 + /// Message to log in 488 + /// 489 + /// In en, this message translates to: 490 + /// **'Please log in to view your profile'** 491 + String get messagePleaseLogin; 492 + 493 + /// Message to log in for blocked users 494 + /// 495 + /// In en, this message translates to: 496 + /// **'Please log in to view blocked users'** 497 + String get messagePleaseLoginBlocked; 498 + 499 + /// Posted time ago message 500 + /// 501 + /// In en, this message translates to: 502 + /// **'Posted {time} ago'** 503 + String messagePostedAgo(String time); 504 + 505 + /// Show replies message 506 + /// 507 + /// In en, this message translates to: 508 + /// **'Show {count} replies'** 509 + String messageShowReplies(int count); 510 + 511 + /// Auto delete stories message 512 + /// 513 + /// In en, this message translates to: 514 + /// **'Stories auto-delete after 24 hours'** 515 + String get messageAutoDeleteStories; 516 + 517 + /// Detailed description for the auto delete stories setting, including the initial cleanup warning 518 + /// 519 + /// In en, this message translates to: 520 + /// **'Stories are public and stored on your PDS indefinitely. Enable this so the app auto deletes them forever after 24h. Enabling this will also execute an initial cleanup of any stories older than 24h.'** 521 + String get messageAutoDeleteStoriesDescription; 522 + 523 + /// Exporting video progress message 524 + /// 525 + /// In en, this message translates to: 526 + /// **'Exporting video…'** 527 + String get messageExportingVideo; 528 + 529 + /// Story number label 530 + /// 531 + /// In en, this message translates to: 532 + /// **'Story {number}'** 533 + String messageStoryNumber(int number); 534 + 535 + /// Manage tooltip 536 + /// 537 + /// In en, this message translates to: 538 + /// **'Manage'** 539 + String get tooltipManage; 540 + 541 + /// Delete tooltip 542 + /// 543 + /// In en, this message translates to: 544 + /// **'Delete'** 545 + String get tooltipDelete; 546 + 547 + /// Retry tooltip 548 + /// 549 + /// In en, this message translates to: 550 + /// **'Retry'** 551 + String get tooltipRetry; 552 + 553 + /// Label settings tooltip 554 + /// 555 + /// In en, this message translates to: 556 + /// **'Label settings'** 557 + String get tooltipLabelSettings; 558 + 559 + /// Remove labeler tooltip 560 + /// 561 + /// In en, this message translates to: 562 + /// **'Remove labeler'** 563 + String get tooltipRemoveLabeler; 564 + 565 + /// Revert tooltip 566 + /// 567 + /// In en, this message translates to: 568 + /// **'Revert'** 569 + String get tooltipRevert; 570 + 571 + /// Generation time label 572 + /// 573 + /// In en, this message translates to: 574 + /// **'Generation time:'** 575 + String get labelGenerationTime; 576 + 577 + /// Duration label 578 + /// 579 + /// In en, this message translates to: 580 + /// **'Duration:'** 581 + String get labelDuration; 582 + 583 + /// Size label 584 + /// 585 + /// In en, this message translates to: 586 + /// **'Size:'** 587 + String get labelSize; 588 + 589 + /// Resolution label 590 + /// 591 + /// In en, this message translates to: 592 + /// **'Resolution:'** 593 + String get labelResolution; 594 + 595 + /// Add labeler label 596 + /// 597 + /// In en, this message translates to: 598 + /// **'Add Labeler'** 599 + String get labelAddLabeler; 600 + 601 + /// Character count label 602 + /// 603 + /// In en, this message translates to: 604 + /// **'{count}/1000'** 605 + String labelCharacters(int count); 606 + 607 + /// Report category: Violence 608 + /// 609 + /// In en, this message translates to: 610 + /// **'Violence'** 611 + String get categoryViolence; 612 + 613 + /// Report category: Sexual Content 614 + /// 615 + /// In en, this message translates to: 616 + /// **'Sexual Content'** 617 + String get categorySexualContent; 618 + 619 + /// Report category: Child Safety 620 + /// 621 + /// In en, this message translates to: 622 + /// **'Child Safety'** 623 + String get categoryChildSafety; 624 + 625 + /// Report category: Harassment 626 + /// 627 + /// In en, this message translates to: 628 + /// **'Harassment'** 629 + String get categoryHarassment; 630 + 631 + /// Report category: Misleading 632 + /// 633 + /// In en, this message translates to: 634 + /// **'Misleading'** 635 + String get categoryMisleading; 636 + 637 + /// Report category: Rule Violations 638 + /// 639 + /// In en, this message translates to: 640 + /// **'Rule Violations'** 641 + String get categoryRuleViolations; 642 + 643 + /// Report category: Self-Harm 644 + /// 645 + /// In en, this message translates to: 646 + /// **'Self-Harm'** 647 + String get categorySelfHarm; 648 + 649 + /// Report category: Other 650 + /// 651 + /// In en, this message translates to: 652 + /// **'Other'** 653 + String get categoryOther; 174 654 } 175 655 176 656 class _AppLocalizationsDelegate
+252
lib/src/core/l10n/app_localizations_en.dart
··· 36 36 String get buttonClose => 'Close'; 37 37 38 38 @override 39 + String get buttonAdd => 'Add'; 40 + 41 + @override 42 + String get buttonRemove => 'Remove'; 43 + 44 + @override 45 + String get buttonRetry => 'Retry'; 46 + 47 + @override 48 + String get buttonTryAgain => 'Try again'; 49 + 50 + @override 51 + String get buttonGoBack => 'Go Back'; 52 + 53 + @override 54 + String get buttonAllowAccess => 'Allow Access'; 55 + 56 + @override 57 + String get buttonOpenSettings => 'Open Settings'; 58 + 59 + @override 60 + String get buttonSubmit => 'Submit'; 61 + 62 + @override 39 63 String get inputErrorRequired => 'This field is required'; 40 64 41 65 @override ··· 46 70 47 71 @override 48 72 String get searchPlaceholder => 'Search...'; 73 + 74 + @override 75 + String get pageTitleSettings => 'Settings'; 76 + 77 + @override 78 + String get pageTitleStoryManager => 'Story Manager'; 79 + 80 + @override 81 + String get pageTitleSound => 'Sound'; 82 + 83 + @override 84 + String get pageTitleYourFeeds => 'Your Feeds'; 85 + 86 + @override 87 + String get pageTitleBlockedUsers => 'Blocked Users'; 88 + 89 + @override 90 + String get pageTitleEditProfile => 'Edit Profile'; 91 + 92 + @override 93 + String get pageTitleCompleteProfile => 'Complete your profile'; 94 + 95 + @override 96 + String get pageTitleLabelerSettings => 'Labeler Settings'; 97 + 98 + @override 99 + String get pageTitleLabelers => 'Labelers'; 100 + 101 + @override 102 + String get pageTitleResult => 'Result'; 103 + 104 + @override 105 + String get dialogDeleteStory => 'Delete Story'; 106 + 107 + @override 108 + String get dialogDeleteStoryConfirm => 109 + 'Are you sure you want to delete this story?'; 110 + 111 + @override 112 + String get dialogRemoveLabeler => 'Remove Labeler'; 113 + 114 + @override 115 + String get dialogRemoveLabelerConfirm => 116 + 'Are you sure you want to remove this labeler?'; 117 + 118 + @override 119 + String get dialogRemoveFeed => 'Remove Feed'; 120 + 121 + @override 122 + String get dialogDeletePost => 'Delete Post'; 123 + 124 + @override 125 + String get dialogDeleteComment => 'Delete Comment'; 126 + 127 + @override 128 + String get emptyNoUsers => 'No users to display.'; 129 + 130 + @override 131 + String get emptyNoBlockedUsers => 'No blocked users.'; 132 + 133 + @override 134 + String get emptyNoStories => 'No stories'; 135 + 136 + @override 137 + String get emptyNoComments => 'No comments yet.'; 138 + 139 + @override 140 + String get emptyNoCrosspostComments => 'No crosspost comments yet.'; 141 + 142 + @override 143 + String get emptyNoLabelers => 'No Labelers'; 144 + 145 + @override 146 + String get emptyNoLabelersDescription => 147 + 'Add labelers to customize content moderation'; 148 + 149 + @override 150 + String get emptyDiscoverContent => 'Discover new content'; 151 + 152 + @override 153 + String get emptyNoMedia => 'No photos or videos found...'; 154 + 155 + @override 156 + String get errorGeneric => 'An error occurred'; 157 + 158 + @override 159 + String get errorLoadingFeed => 'Error loading feed'; 160 + 161 + @override 162 + String get errorLoadingSound => 'Error loading sound'; 163 + 164 + @override 165 + String get errorLoadingReposts => 'Error loading reposts'; 166 + 167 + @override 168 + String get errorLoadingLikes => 'Error loading likes'; 169 + 170 + @override 171 + String get errorLoadingPosts => 'Error loading posts'; 172 + 173 + @override 174 + String get hintSearchUsersPosts => 'Search users, posts...'; 175 + 176 + @override 177 + String get hintSearchUsers => 'Search users'; 178 + 179 + @override 180 + String get hintMessage => 'Message...'; 181 + 182 + @override 183 + String get hintTypeMessage => 'Type a message...'; 184 + 185 + @override 186 + String get hintAddDescription => 'Add a description... (optional)'; 187 + 188 + @override 189 + String get hintAddAltText => 'Add alt text'; 190 + 191 + @override 192 + String get hintDisplayName => 'Display Name'; 193 + 194 + @override 195 + String get hintBio => 'Bio'; 196 + 197 + @override 198 + String get hintDidOrHandle => 'DID or Handle'; 199 + 200 + @override 201 + String get hintDidOrHandleExample => 'did:plc:... or @handle.bsky.social'; 202 + 203 + @override 204 + String get hintAdditionalDetails => 'Additional details (optional)'; 205 + 206 + @override 207 + String get hintImageDescription => 'Image Description'; 208 + 209 + @override 210 + String get messagePleaseLogin => 'Please log in to view your profile'; 211 + 212 + @override 213 + String get messagePleaseLoginBlocked => 'Please log in to view blocked users'; 214 + 215 + @override 216 + String messagePostedAgo(String time) { 217 + return 'Posted $time ago'; 218 + } 219 + 220 + @override 221 + String messageShowReplies(int count) { 222 + return 'Show $count replies'; 223 + } 224 + 225 + @override 226 + String get messageAutoDeleteStories => 'Stories auto-delete after 24 hours'; 227 + 228 + @override 229 + String get messageAutoDeleteStoriesDescription => 230 + 'Stories are public and stored on your PDS indefinitely. Enable this so the app auto deletes them forever after 24h. Enabling this will also execute an initial cleanup of any stories older than 24h.'; 231 + 232 + @override 233 + String get messageExportingVideo => 'Exporting video…'; 234 + 235 + @override 236 + String messageStoryNumber(int number) { 237 + return 'Story $number'; 238 + } 239 + 240 + @override 241 + String get tooltipManage => 'Manage'; 242 + 243 + @override 244 + String get tooltipDelete => 'Delete'; 245 + 246 + @override 247 + String get tooltipRetry => 'Retry'; 248 + 249 + @override 250 + String get tooltipLabelSettings => 'Label settings'; 251 + 252 + @override 253 + String get tooltipRemoveLabeler => 'Remove labeler'; 254 + 255 + @override 256 + String get tooltipRevert => 'Revert'; 257 + 258 + @override 259 + String get labelGenerationTime => 'Generation time:'; 260 + 261 + @override 262 + String get labelDuration => 'Duration:'; 263 + 264 + @override 265 + String get labelSize => 'Size:'; 266 + 267 + @override 268 + String get labelResolution => 'Resolution:'; 269 + 270 + @override 271 + String get labelAddLabeler => 'Add Labeler'; 272 + 273 + @override 274 + String labelCharacters(int count) { 275 + return '$count/1000'; 276 + } 277 + 278 + @override 279 + String get categoryViolence => 'Violence'; 280 + 281 + @override 282 + String get categorySexualContent => 'Sexual Content'; 283 + 284 + @override 285 + String get categoryChildSafety => 'Child Safety'; 286 + 287 + @override 288 + String get categoryHarassment => 'Harassment'; 289 + 290 + @override 291 + String get categoryMisleading => 'Misleading'; 292 + 293 + @override 294 + String get categoryRuleViolations => 'Rule Violations'; 295 + 296 + @override 297 + String get categorySelfHarm => 'Self-Harm'; 298 + 299 + @override 300 + String get categoryOther => 'Other'; 49 301 }
+420
lib/src/core/l10n/intl_en.arb
··· 47 47 "description": "Close option in options panel" 48 48 }, 49 49 50 + "buttonAdd": "Add", 51 + "@buttonAdd": { 52 + "description": "Add button text" 53 + }, 54 + 55 + "buttonRemove": "Remove", 56 + "@buttonRemove": { 57 + "description": "Remove button text" 58 + }, 59 + 60 + "buttonRetry": "Retry", 61 + "@buttonRetry": { 62 + "description": "Retry button text" 63 + }, 64 + 65 + "buttonTryAgain": "Try again", 66 + "@buttonTryAgain": { 67 + "description": "Try again button text" 68 + }, 69 + 70 + "buttonGoBack": "Go Back", 71 + "@buttonGoBack": { 72 + "description": "Go back button text" 73 + }, 74 + 75 + "buttonAllowAccess": "Allow Access", 76 + "@buttonAllowAccess": { 77 + "description": "Allow access button for permissions" 78 + }, 79 + 80 + "buttonOpenSettings": "Open Settings", 81 + "@buttonOpenSettings": { 82 + "description": "Open settings button" 83 + }, 84 + 85 + "buttonSubmit": "Submit", 86 + "@buttonSubmit": { 87 + "description": "Submit button text" 88 + }, 89 + 50 90 "inputErrorRequired": "This field is required", 51 91 "@inputErrorRequired": { 52 92 "description": "Error message for required fields" ··· 65 105 "searchPlaceholder": "Search...", 66 106 "@searchPlaceholder": { 67 107 "description": "Placeholder text for search inputs" 108 + }, 109 + 110 + "pageTitleSettings": "Settings", 111 + "@pageTitleSettings": { 112 + "description": "Settings page title" 113 + }, 114 + 115 + "pageTitleStoryManager": "Story Manager", 116 + "@pageTitleStoryManager": { 117 + "description": "Story manager page title" 118 + }, 119 + 120 + "pageTitleSound": "Sound", 121 + "@pageTitleSound": { 122 + "description": "Sound page title" 123 + }, 124 + 125 + "pageTitleYourFeeds": "Your Feeds", 126 + "@pageTitleYourFeeds": { 127 + "description": "Your feeds page title" 128 + }, 129 + 130 + "pageTitleBlockedUsers": "Blocked Users", 131 + "@pageTitleBlockedUsers": { 132 + "description": "Blocked users page title" 133 + }, 134 + 135 + "pageTitleEditProfile": "Edit Profile", 136 + "@pageTitleEditProfile": { 137 + "description": "Edit profile page title" 138 + }, 139 + 140 + "pageTitleCompleteProfile": "Complete your profile", 141 + "@pageTitleCompleteProfile": { 142 + "description": "Complete profile page title" 143 + }, 144 + 145 + "pageTitleLabelerSettings": "Labeler Settings", 146 + "@pageTitleLabelerSettings": { 147 + "description": "Labeler settings page title" 148 + }, 149 + 150 + "pageTitleLabelers": "Labelers", 151 + "@pageTitleLabelers": { 152 + "description": "Labelers page title" 153 + }, 154 + 155 + "pageTitleResult": "Result", 156 + "@pageTitleResult": { 157 + "description": "Result page title" 158 + }, 159 + 160 + "dialogDeleteStory": "Delete Story", 161 + "@dialogDeleteStory": { 162 + "description": "Delete story dialog title" 163 + }, 164 + 165 + "dialogDeleteStoryConfirm": "Are you sure you want to delete this story?", 166 + "@dialogDeleteStoryConfirm": { 167 + "description": "Delete story confirmation message" 168 + }, 169 + 170 + "dialogRemoveLabeler": "Remove Labeler", 171 + "@dialogRemoveLabeler": { 172 + "description": "Remove labeler dialog title" 173 + }, 174 + 175 + "dialogRemoveLabelerConfirm": "Are you sure you want to remove this labeler?", 176 + "@dialogRemoveLabelerConfirm": { 177 + "description": "Remove labeler confirmation message" 178 + }, 179 + 180 + "dialogRemoveFeed": "Remove Feed", 181 + "@dialogRemoveFeed": { 182 + "description": "Remove feed dialog title" 183 + }, 184 + 185 + "dialogDeletePost": "Delete Post", 186 + "@dialogDeletePost": { 187 + "description": "Delete post dialog title" 188 + }, 189 + 190 + "dialogDeleteComment": "Delete Comment", 191 + "@dialogDeleteComment": { 192 + "description": "Delete comment dialog title" 193 + }, 194 + 195 + "emptyNoUsers": "No users to display.", 196 + "@emptyNoUsers": { 197 + "description": "Empty state for no users" 198 + }, 199 + 200 + "emptyNoBlockedUsers": "No blocked users.", 201 + "@emptyNoBlockedUsers": { 202 + "description": "Empty state for no blocked users" 203 + }, 204 + 205 + "emptyNoStories": "No stories", 206 + "@emptyNoStories": { 207 + "description": "Empty state for no stories" 208 + }, 209 + 210 + "emptyNoComments": "No comments yet.", 211 + "@emptyNoComments": { 212 + "description": "Empty state for no comments" 213 + }, 214 + 215 + "emptyNoCrosspostComments": "No crosspost comments yet.", 216 + "@emptyNoCrosspostComments": { 217 + "description": "Empty state for no crosspost comments" 218 + }, 219 + 220 + "emptyNoLabelers": "No Labelers", 221 + "@emptyNoLabelers": { 222 + "description": "Empty state for no labelers" 223 + }, 224 + 225 + "emptyNoLabelersDescription": "Add labelers to customize content moderation", 226 + "@emptyNoLabelersDescription": { 227 + "description": "Description for empty labelers state" 228 + }, 229 + 230 + "emptyDiscoverContent": "Discover new content", 231 + "@emptyDiscoverContent": { 232 + "description": "Empty state for discover content" 233 + }, 234 + 235 + "emptyNoMedia": "No photos or videos found...", 236 + "@emptyNoMedia": { 237 + "description": "Empty state for no media" 238 + }, 239 + 240 + "errorGeneric": "An error occurred", 241 + "@errorGeneric": { 242 + "description": "Generic error message" 243 + }, 244 + 245 + "errorLoadingFeed": "Error loading feed", 246 + "@errorLoadingFeed": { 247 + "description": "Error loading feed message" 248 + }, 249 + 250 + "errorLoadingSound": "Error loading sound", 251 + "@errorLoadingSound": { 252 + "description": "Error loading sound message" 253 + }, 254 + 255 + "errorLoadingReposts": "Error loading reposts", 256 + "@errorLoadingReposts": { 257 + "description": "Error loading reposts message" 258 + }, 259 + 260 + "errorLoadingLikes": "Error loading likes", 261 + "@errorLoadingLikes": { 262 + "description": "Error loading likes message" 263 + }, 264 + 265 + "errorLoadingPosts": "Error loading posts", 266 + "@errorLoadingPosts": { 267 + "description": "Error loading posts message" 268 + }, 269 + 270 + "hintSearchUsersPosts": "Search users, posts...", 271 + "@hintSearchUsersPosts": { 272 + "description": "Search placeholder for users and posts" 273 + }, 274 + 275 + "hintSearchUsers": "Search users", 276 + "@hintSearchUsers": { 277 + "description": "Search placeholder for users" 278 + }, 279 + 280 + "hintMessage": "Message...", 281 + "@hintMessage": { 282 + "description": "Message input placeholder" 283 + }, 284 + 285 + "hintTypeMessage": "Type a message...", 286 + "@hintTypeMessage": { 287 + "description": "Type message input placeholder" 288 + }, 289 + 290 + "hintAddDescription": "Add a description... (optional)", 291 + "@hintAddDescription": { 292 + "description": "Add description placeholder" 293 + }, 294 + 295 + "hintAddAltText": "Add alt text", 296 + "@hintAddAltText": { 297 + "description": "Add alt text placeholder" 298 + }, 299 + 300 + "hintDisplayName": "Display Name", 301 + "@hintDisplayName": { 302 + "description": "Display name input label" 303 + }, 304 + 305 + "hintBio": "Bio", 306 + "@hintBio": { 307 + "description": "Bio input label" 308 + }, 309 + 310 + "hintDidOrHandle": "DID or Handle", 311 + "@hintDidOrHandle": { 312 + "description": "DID or handle input label" 313 + }, 314 + 315 + "hintDidOrHandleExample": "did:plc:... or @handle.bsky.social", 316 + "@hintDidOrHandleExample": { 317 + "description": "DID or handle input hint" 318 + }, 319 + 320 + "hintAdditionalDetails": "Additional details (optional)", 321 + "@hintAdditionalDetails": { 322 + "description": "Additional details input placeholder" 323 + }, 324 + 325 + "hintImageDescription": "Image Description", 326 + "@hintImageDescription": { 327 + "description": "Image description input label" 328 + }, 329 + 330 + "messagePleaseLogin": "Please log in to view your profile", 331 + "@messagePleaseLogin": { 332 + "description": "Message to log in" 333 + }, 334 + 335 + "messagePleaseLoginBlocked": "Please log in to view blocked users", 336 + "@messagePleaseLoginBlocked": { 337 + "description": "Message to log in for blocked users" 338 + }, 339 + 340 + "messagePostedAgo": "Posted {time} ago", 341 + "@messagePostedAgo": { 342 + "description": "Posted time ago message", 343 + "placeholders": { 344 + "time": { 345 + "type": "String" 346 + } 347 + } 348 + }, 349 + 350 + "messageShowReplies": "Show {count} replies", 351 + "@messageShowReplies": { 352 + "description": "Show replies message", 353 + "placeholders": { 354 + "count": { 355 + "type": "int" 356 + } 357 + } 358 + }, 359 + 360 + "messageAutoDeleteStories": "Stories auto-delete after 24 hours", 361 + "@messageAutoDeleteStories": { 362 + "description": "Auto delete stories message" 363 + }, 364 + 365 + "messageAutoDeleteStoriesDescription": "Stories are public and stored on your PDS indefinitely. Enable this so the app auto deletes them forever after 24h. Enabling this will also execute an initial cleanup of any stories older than 24h.", 366 + "@messageAutoDeleteStoriesDescription": { 367 + "description": "Detailed description for the auto delete stories setting, including the initial cleanup warning" 368 + }, 369 + 370 + "messageExportingVideo": "Exporting video…", 371 + "@messageExportingVideo": { 372 + "description": "Exporting video progress message" 373 + }, 374 + 375 + "messageStoryNumber": "Story {number}", 376 + "@messageStoryNumber": { 377 + "description": "Story number label", 378 + "placeholders": { 379 + "number": { 380 + "type": "int" 381 + } 382 + } 383 + }, 384 + 385 + "tooltipManage": "Manage", 386 + "@tooltipManage": { 387 + "description": "Manage tooltip" 388 + }, 389 + 390 + "tooltipDelete": "Delete", 391 + "@tooltipDelete": { 392 + "description": "Delete tooltip" 393 + }, 394 + 395 + "tooltipRetry": "Retry", 396 + "@tooltipRetry": { 397 + "description": "Retry tooltip" 398 + }, 399 + 400 + "tooltipLabelSettings": "Label settings", 401 + "@tooltipLabelSettings": { 402 + "description": "Label settings tooltip" 403 + }, 404 + 405 + "tooltipRemoveLabeler": "Remove labeler", 406 + "@tooltipRemoveLabeler": { 407 + "description": "Remove labeler tooltip" 408 + }, 409 + 410 + "tooltipRevert": "Revert", 411 + "@tooltipRevert": { 412 + "description": "Revert tooltip" 413 + }, 414 + 415 + "labelGenerationTime": "Generation time:", 416 + "@labelGenerationTime": { 417 + "description": "Generation time label" 418 + }, 419 + 420 + "labelDuration": "Duration:", 421 + "@labelDuration": { 422 + "description": "Duration label" 423 + }, 424 + 425 + "labelSize": "Size:", 426 + "@labelSize": { 427 + "description": "Size label" 428 + }, 429 + 430 + "labelResolution": "Resolution:", 431 + "@labelResolution": { 432 + "description": "Resolution label" 433 + }, 434 + 435 + "labelAddLabeler": "Add Labeler", 436 + "@labelAddLabeler": { 437 + "description": "Add labeler label" 438 + }, 439 + 440 + "labelCharacters": "{count}/1000", 441 + "@labelCharacters": { 442 + "description": "Character count label", 443 + "placeholders": { 444 + "count": { 445 + "type": "int" 446 + } 447 + } 448 + }, 449 + 450 + "categoryViolence": "Violence", 451 + "@categoryViolence": { 452 + "description": "Report category: Violence" 453 + }, 454 + 455 + "categorySexualContent": "Sexual Content", 456 + "@categorySexualContent": { 457 + "description": "Report category: Sexual Content" 458 + }, 459 + 460 + "categoryChildSafety": "Child Safety", 461 + "@categoryChildSafety": { 462 + "description": "Report category: Child Safety" 463 + }, 464 + 465 + "categoryHarassment": "Harassment", 466 + "@categoryHarassment": { 467 + "description": "Report category: Harassment" 468 + }, 469 + 470 + "categoryMisleading": "Misleading", 471 + "@categoryMisleading": { 472 + "description": "Report category: Misleading" 473 + }, 474 + 475 + "categoryRuleViolations": "Rule Violations", 476 + "@categoryRuleViolations": { 477 + "description": "Report category: Rule Violations" 478 + }, 479 + 480 + "categorySelfHarm": "Self-Harm", 481 + "@categorySelfHarm": { 482 + "description": "Report category: Self-Harm" 483 + }, 484 + 485 + "categoryOther": "Other", 486 + "@categoryOther": { 487 + "description": "Report category: Other" 68 488 } 69 489 }
+18 -19
lib/src/features/auth/ui/pages/onboarding_page.dart
··· 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 4 import 'package:spark/src/core/auth/data/models/onboarding_screen_state.dart'; 5 + import 'package:spark/src/core/l10n/app_localizations.dart'; 5 6 import 'package:spark/src/core/routing/app_router.dart'; 6 7 import 'package:spark/src/core/ui/widgets/custom_text_field.dart'; 7 8 import 'package:spark/src/features/auth/providers/onboarding_notifier.dart'; ··· 109 110 110 111 @override 111 112 Widget build(BuildContext context) { 113 + final l10n = AppLocalizations.of(context); 112 114 final onboardingStateAsync = ref.watch(onboardingProvider); 113 115 final notifier = ref.read(onboardingProvider.notifier); 114 116 115 - // Initialize controllers from state if not already set 116 117 ref.listen<AsyncValue<OnboardingScreenState>>(onboardingProvider, ( 117 118 previous, 118 119 next, ··· 139 140 backgroundColor: theme.scaffoldBackgroundColor, 140 141 elevation: 0, 141 142 centerTitle: true, 142 - title: const Text('Complete your profile'), 143 - leading: const AutoLeadingButton(), // Adds back button if applicable 143 + title: Text(l10n.pageTitleCompleteProfile), 144 + leading: const AutoLeadingButton(), 144 145 ), 145 146 body: onboardingStateAsync.when( 146 147 loading: () => const Center(child: CircularProgressIndicator()), ··· 148 149 child: Column( 149 150 mainAxisSize: MainAxisSize.min, 150 151 children: [ 151 - Text('Error: $err'), 152 + Text('${l10n.errorGeneric}: $err'), 152 153 const SizedBox(height: 8), 153 154 ElevatedButton( 154 155 onPressed: notifier.reloadProfile, 155 - child: const Text('Retry'), 156 + child: Text(l10n.buttonRetry), 156 157 ), 157 158 ], 158 159 ), ··· 188 189 child: CircleAvatar( 189 190 radius: 50, 190 191 backgroundImage: avatarImageProvider, 191 - backgroundColor: theme 192 - .colorScheme 193 - .surfaceContainerHighest, // Placeholder color 192 + backgroundColor: 193 + theme.colorScheme.surfaceContainerHighest, 194 194 child: avatarImageProvider == null 195 195 ? Icon( 196 196 Icons.person, ··· 201 201 ), 202 202 ), 203 203 Positioned( 204 - // Positioned to avoid overlap if both are shown 205 204 right: 0, 206 205 bottom: 0, 207 206 child: Row( 208 207 mainAxisSize: MainAxisSize.min, 209 208 children: [ 210 - if (hasLocalAvatar) // If true, show undo button 209 + if (hasLocalAvatar) 211 210 Padding( 212 211 padding: const EdgeInsets.all(4), 213 212 child: GestureDetector( ··· 226 225 ), 227 226 ), 228 227 ), 229 - if (isAvatarActive) // If true, show close 228 + if (isAvatarActive) 230 229 Padding( 231 230 padding: const EdgeInsets.all(4), 232 231 child: GestureDetector( ··· 259 258 children: [ 260 259 CustomTextField( 261 260 controller: _displayNameController, 262 - hintText: 'Display Name', 261 + hintText: l10n.hintDisplayName, 263 262 fillColor: 264 263 theme.inputDecorationTheme.fillColor ?? 265 264 theme.colorScheme.surface, ··· 272 271 : null, 273 272 validator: (value) { 274 273 if (value == null || value.trim().isEmpty) { 275 - return 'Display Name is required'; 274 + return l10n.inputErrorRequired; 276 275 } 277 276 if (value.trim().length > 64) { 278 277 return 'Display Name cannot exceed ' ··· 284 283 const SizedBox(height: 12), 285 284 CustomTextField( 286 285 controller: _descriptionController, 287 - hintText: 'Bio', 286 + hintText: l10n.hintBio, 288 287 fillColor: 289 288 theme.inputDecorationTheme.fillColor ?? 290 289 theme.colorScheme.surface, ··· 300 299 if (value != null && value.trim().length > 256) { 301 300 return 'Bio cannot exceed 256 characters'; 302 301 } 303 - return null; // Bio is optional 302 + return null; 304 303 }, 305 304 ), 306 305 const SizedBox(height: 24), ··· 329 328 strokeWidth: 2, 330 329 ), 331 330 ) 332 - : const Row( 331 + : Row( 333 332 mainAxisSize: MainAxisSize.min, 334 333 children: [ 335 - Text('Complete'), 336 - SizedBox(width: 8), 337 - Icon(Icons.check), 334 + Text(l10n.buttonConfirm), 335 + const SizedBox(width: 8), 336 + const Icon(Icons.check), 338 337 ], 339 338 ), 340 339 ),
+4 -3
lib/src/features/comments/ui/pages/comments_page.dart
··· 3 3 import 'package:fluentui_system_icons/fluentui_system_icons.dart'; 4 4 import 'package:flutter/material.dart'; 5 5 import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 + import 'package:spark/src/core/l10n/app_localizations.dart'; 6 7 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 7 8 import 'package:spark/src/core/routing/app_router.dart'; 8 9 import 'package:spark/src/features/comments/providers/comments_page_provider.dart'; ··· 216 217 217 218 @override 218 219 Widget build(BuildContext context) { 220 + final l10n = AppLocalizations.of(context); 219 221 final asyncState = ref.watch(commentsPageProvider(postUri: _postAtUri)); 220 222 final threadPost = asyncState.value?.thread.post; 221 223 final displayPost = ··· 271 273 child: asyncState.when( 272 274 data: (data) { 273 275 if (data.thread.replies == null || data.thread.replies!.isEmpty) { 274 - return const Center(child: Text('No comments yet.')); 276 + return Center(child: Text(l10n.emptyNoComments)); 275 277 } 276 278 277 - // Find the index of the highlighted reply and scroll to it 278 279 if (_highlightedReplyUri != null && !_hasScrolledToHighlighted) { 279 280 WidgetsBinding.instance.addPostFrameCallback((_) { 280 281 _scrollToHighlightedReply(data.thread.replies!); ··· 300 301 ); 301 302 }, 302 303 error: (error, stackTrace) { 303 - return Center(child: Text('Error: $error')); 304 + return Center(child: Text('${l10n.errorGeneric}: $error')); 304 305 }, 305 306 loading: () { 306 307 return const Center(child: CircularProgressIndicator());
+10 -7
lib/src/features/comments/ui/widgets/comment_item.dart
··· 81 81 } 82 82 83 83 void _handleDeleteComment() { 84 - // Confirm deletion 84 + final l10n = AppLocalizations.of(context); 85 85 showDialog( 86 86 context: context, 87 87 builder: (context) => AlertDialog( 88 - title: const Text('Delete Comment'), 89 - content: const Text( 88 + title: Text(l10n.dialogDeleteComment), 89 + content: Text( 90 90 'Are you sure you want to delete this comment? This action ' 91 91 'cannot be undone.', 92 92 ), 93 93 actions: [ 94 94 TextButton( 95 95 onPressed: () => context.router.maybePop(), 96 - child: Text(AppLocalizations.of(context).buttonCancel), 96 + child: Text(l10n.buttonCancel), 97 97 ), 98 98 TextButton( 99 99 onPressed: () async { ··· 106 106 ) 107 107 .deleteComment(commentState.thread.post.uri.toString()); 108 108 if (context.mounted) { 109 - await context.router.maybePop(); // to close the menu below 109 + await context.router.maybePop(); 110 110 } 111 111 } catch (e) { 112 112 _logger.e('Error deleting comment', error: e); 113 113 } 114 114 }, 115 - child: Text(AppLocalizations.of(context).buttonDelete), 115 + child: Text(l10n.buttonDelete), 116 116 ), 117 117 ], 118 118 ), ··· 294 294 295 295 @override 296 296 Widget build(BuildContext context) { 297 + final l10n = AppLocalizations.of(context); 297 298 return GestureDetector( 298 299 onTap: () => context.router.push( 299 300 RepliesRoute(postUri: commentState.thread.post.uri.toString()), ··· 306 307 left: BorderSide(color: Theme.of(context).colorScheme.surface), 307 308 ), 308 309 ), 309 - child: Text('Show ${commentState.thread.post.replyCount} replies'), 310 + child: Text( 311 + l10n.messageShowReplies(commentState.thread.post.replyCount ?? 0), 312 + ), 310 313 ), 311 314 ); 312 315 }
+8 -6
lib/src/features/profile/ui/pages/blocks_page.dart
··· 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 4 import 'package:spark/src/core/design_system/components/atoms/buttons/app_leading_button.dart'; 5 + import 'package:spark/src/core/l10n/app_localizations.dart'; 5 6 import 'package:spark/src/features/auth/providers/auth_providers.dart'; 6 7 import 'package:spark/src/features/profile/providers/blocks_provider.dart'; 7 8 import 'package:spark/src/features/profile/ui/widgets/blocks_list_view.dart'; ··· 44 45 @override 45 46 Widget build(BuildContext context) { 46 47 final currentDid = ref.watch(currentDidProvider); 48 + final l10n = AppLocalizations.of(context); 47 49 48 50 if (currentDid == null) { 49 51 return Scaffold( 50 52 appBar: AppBar( 51 - leading: const AppLeadingButton(tooltip: 'Back'), 52 - title: const Text('Blocked Users'), 53 + leading: AppLeadingButton(tooltip: l10n.buttonCancel), 54 + title: Text(l10n.pageTitleBlockedUsers), 53 55 ), 54 - body: const Center(child: Text('Please log in to view blocked users')), 56 + body: Center(child: Text(l10n.messagePleaseLoginBlocked)), 55 57 ); 56 58 } 57 59 ··· 59 61 60 62 return Scaffold( 61 63 appBar: AppBar( 62 - leading: const AppLeadingButton(tooltip: 'Back'), 63 - title: const Text('Blocked Users'), 64 + leading: AppLeadingButton(tooltip: l10n.buttonCancel), 65 + title: Text(l10n.pageTitleBlockedUsers), 64 66 ), 65 67 body: RefreshIndicator( 66 68 onRefresh: () async { ··· 81 83 Center( 82 84 child: Padding( 83 85 padding: const EdgeInsets.all(16), 84 - child: Text('An error occurred: $error'), 86 + child: Text('${l10n.errorGeneric}: $error'), 85 87 ), 86 88 ), 87 89 ],
+10 -11
lib/src/features/profile/ui/pages/edit_profile_page.dart
··· 3 3 import 'package:auto_route/auto_route.dart'; 4 4 import 'package:flutter/material.dart'; 5 5 import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 + import 'package:spark/src/core/l10n/app_localizations.dart'; 6 7 import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; 7 8 import 'package:spark/src/core/ui/widgets/custom_text_field.dart'; 8 9 import 'package:spark/src/features/profile/providers/edit_profile_provider.dart'; ··· 92 93 93 94 @override 94 95 Widget build(BuildContext context) { 96 + final l10n = AppLocalizations.of(context); 95 97 final editProfileState = ref.watch(editProfileProvider(widget.profile)); 96 98 final editProfileNotifier = ref.read( 97 99 editProfileProvider(widget.profile).notifier, ··· 100 102 final theme = Theme.of(context); 101 103 final colorScheme = theme.colorScheme; 102 104 103 - // Update controllers if state changes externally 104 105 ref.listen(editProfileProvider(widget.profile), (_, next) { 105 106 if (_displayNameController.text != next.displayName) { 106 107 _displayNameController.text = next.displayName; ··· 110 111 } 111 112 }); 112 113 113 - // Determine which avatar to display 114 114 ImageProvider<Object>? avatarImageProvider; 115 115 116 116 if (editProfileState.localAvatar != null) { ··· 124 124 editProfileState.localAvatar as String, 125 125 ); 126 126 } else { 127 - // Handle AtUri case for localAvatar 128 127 if (editProfileState.localAvatar.toString().isNotEmpty) { 129 128 final avatarUrl = editProfileState.localAvatar.toString(); 130 129 avatarImageProvider = NetworkImage(avatarUrl); ··· 144 143 return Scaffold( 145 144 backgroundColor: theme.scaffoldBackgroundColor, 146 145 appBar: AppBar( 147 - title: const Text('Edit Profile'), 146 + title: Text(l10n.pageTitleEditProfile), 148 147 backgroundColor: theme.scaffoldBackgroundColor, 149 148 elevation: 0, 150 149 centerTitle: true, ··· 218 217 children: [ 219 218 CustomTextField( 220 219 controller: _displayNameController, 221 - hintText: 'Display Name', 220 + hintText: l10n.hintDisplayName, 222 221 fillColor: 223 222 theme.inputDecorationTheme.fillColor ?? 224 223 theme.colorScheme.surface, ··· 236 235 : null, 237 236 validator: (value) { 238 237 if (value == null || value.trim().isEmpty) { 239 - return 'Display Name is required'; 238 + return l10n.inputErrorRequired; 240 239 } 241 240 if (value.trim().length > 64) { 242 241 return 'Display Name cannot exceed 64 characters'; ··· 247 246 const SizedBox(height: 12), 248 247 CustomTextField( 249 248 controller: _descriptionController, 250 - hintText: 'Bio', 249 + hintText: l10n.hintBio, 251 250 fillColor: 252 251 theme.inputDecorationTheme.fillColor ?? 253 252 theme.colorScheme.surface, ··· 295 294 strokeWidth: 2, 296 295 ), 297 296 ) 298 - : const Row( 297 + : Row( 299 298 mainAxisSize: MainAxisSize.min, 300 299 children: [ 301 - Text('Save'), 302 - SizedBox(width: 8), 303 - Icon(Icons.save), 300 + Text(l10n.buttonSave), 301 + const SizedBox(width: 8), 302 + const Icon(Icons.save), 304 303 ], 305 304 ), 306 305 ),
+3 -1
lib/src/features/profile/ui/widgets/blocks_list_view.dart
··· 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 4 import 'package:spark/src/core/design_system/components/molecules/profile_card.dart'; 5 + import 'package:spark/src/core/l10n/app_localizations.dart'; 5 6 import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; 6 7 import 'package:spark/src/core/routing/app_router.dart'; 7 8 import 'package:spark/src/features/profile/providers/blocks_provider.dart'; ··· 22 23 23 24 @override 24 25 Widget build(BuildContext context, WidgetRef ref) { 26 + final l10n = AppLocalizations.of(context); 25 27 if (users.isEmpty) { 26 - return const Center(child: Text('No blocked users.')); 28 + return Center(child: Text(l10n.emptyNoBlockedUsers)); 27 29 } 28 30 29 31 return ListView.builder(
+5 -3
lib/src/features/profile/ui/widgets/user_list_view.dart
··· 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 4 import 'package:spark/src/core/design_system/components/molecules/profile_card.dart'; 5 + import 'package:spark/src/core/l10n/app_localizations.dart'; 5 6 import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; 6 7 import 'package:spark/src/core/routing/app_router.dart'; 7 8 import 'package:spark/src/features/profile/providers/user_list_provider.dart'; ··· 25 26 26 27 @override 27 28 Widget build(BuildContext context, WidgetRef ref) { 29 + final l10n = AppLocalizations.of(context); 28 30 if (users.isEmpty) { 29 31 return ListView( 30 32 physics: const AlwaysScrollableScrollPhysics(), 31 33 controller: scrollController, 32 - children: const [ 34 + children: [ 33 35 Center( 34 36 child: Padding( 35 - padding: EdgeInsets.all(16), 36 - child: Text('No users to display.'), 37 + padding: const EdgeInsets.all(16), 38 + child: Text(l10n.emptyNoUsers), 37 39 ), 38 40 ), 39 41 ],
+7 -5
lib/src/features/search/ui/pages/search_page.dart
··· 6 6 import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 7 import 'package:spark/src/core/design_system/components/molecules/input_field.dart'; 8 8 import 'package:spark/src/core/design_system/templates/explore_page_template.dart'; 9 + import 'package:spark/src/core/l10n/app_localizations.dart'; 9 10 import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; 10 11 import 'package:spark/src/core/routing/app_router.dart'; 11 12 import 'package:spark/src/features/search/providers/actor_typeahead_provider.dart'; ··· 99 100 100 101 @override 101 102 Widget build(BuildContext context) { 103 + final l10n = AppLocalizations.of(context); 102 104 final userSearchState = ref.watch(searchProvider); 103 105 final postSearchState = ref.watch(postSearchProvider); 104 106 final typeaheadState = ref.watch(actorTypeaheadProvider); ··· 113 115 child: ExplorePageTemplate( 114 116 searchWidget: InputField.search( 115 117 controller: _searchController, 116 - hintText: 'Search users, posts...', 118 + hintText: l10n.hintSearchUsersPosts, 117 119 onSubmitted: _onSubmitted, 118 120 textInputAction: TextInputAction.search, 119 121 leadingWidgets: const [Icon(FluentIcons.search_24_regular, size: 20)], ··· 147 149 ..invalidate(storiesByAuthorProvider()) 148 150 ..invalidate(suggestedFeedsProvider); 149 151 }, 150 - child: const CustomScrollView( 152 + child: CustomScrollView( 151 153 slivers: [ 152 - SliverToBoxAdapter(child: StoriesList()), 153 - SliverToBoxAdapter(child: SuggestedFeedsList()), 154 + const SliverToBoxAdapter(child: StoriesList()), 155 + const SliverToBoxAdapter(child: SuggestedFeedsList()), 154 156 SliverFillRemaining( 155 157 hasScrollBody: false, 156 - child: Center(child: Text('Discover new content')), 158 + child: Center(child: Text(l10n.emptyDiscoverContent)), 157 159 ), 158 160 ], 159 161 ),
+4 -2
lib/src/features/settings/ui/pages/feed_list_page.dart
··· 5 5 import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 6 import 'package:spark/src/core/design_system/components/atoms/buttons/app_leading_button.dart'; 7 7 import 'package:spark/src/core/design_system/components/molecules/settings_feed_card.dart'; 8 + import 'package:spark/src/core/l10n/app_localizations.dart'; 8 9 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 9 10 import 'package:spark/src/features/settings/providers/settings_provider.dart'; 10 11 import 'package:spark/src/features/settings/providers/settings_state.dart'; ··· 27 28 28 29 @override 29 30 Widget build(BuildContext context) { 30 - super.build(context); // Required for AutomaticKeepAliveClientMixin 31 + super.build(context); 31 32 final settingsState = ref.watch(settingsProvider); 32 33 final theme = Theme.of(context); 33 34 final colorScheme = theme.colorScheme; 35 + final l10n = AppLocalizations.of(context); 34 36 35 37 return Scaffold( 36 38 backgroundColor: colorScheme.surface, ··· 39 41 elevation: 0, 40 42 scrolledUnderElevation: 0, 41 43 leading: const AppLeadingButton(), 42 - title: const Text('Your Feeds'), 44 + title: Text(l10n.pageTitleYourFeeds), 43 45 centerTitle: true, 44 46 actions: [ 45 47 TextButton.icon(
+22 -23
lib/src/features/settings/ui/pages/labeler_management_page.dart
··· 102 102 103 103 Future<void> _addLabeler() async { 104 104 final didController = TextEditingController(); 105 + final l10n = AppLocalizations.of(context); 105 106 try { 106 107 final result = await showDialog<String>( 107 108 context: context, 108 109 builder: (context) => AlertDialog( 109 - title: const Text('Add Labeler'), 110 + title: Text(l10n.labelAddLabeler), 110 111 content: Column( 111 112 mainAxisSize: MainAxisSize.min, 112 113 children: [ 113 114 TextField( 114 115 controller: didController, 115 - decoration: const InputDecoration( 116 - labelText: 'DID or Handle', 117 - hintText: 'did:plc:... or @handle.bsky.social', 116 + decoration: InputDecoration( 117 + labelText: l10n.hintDidOrHandle, 118 + hintText: l10n.hintDidOrHandleExample, 118 119 ), 119 120 autofocus: true, 120 121 ), ··· 123 124 actions: [ 124 125 TextButton( 125 126 onPressed: () => Navigator.of(context).pop(), 126 - child: Text(AppLocalizations.of(context).buttonCancel), 127 + child: Text(l10n.buttonCancel), 127 128 ), 128 129 TextButton( 129 130 onPressed: () { ··· 132 133 Navigator.of(context).pop(input); 133 134 } 134 135 }, 135 - child: const Text('Add'), 136 + child: Text(l10n.buttonAdd), 136 137 ), 137 138 ], 138 139 ), ··· 171 172 return; 172 173 } 173 174 175 + final l10n = AppLocalizations.of(context); 174 176 final confirmed = await showDialog<bool>( 175 177 context: context, 176 178 builder: (context) => AlertDialog( 177 - title: const Text('Remove Labeler'), 178 - content: const Text('Are you sure you want to remove this labeler?'), 179 + title: Text(l10n.dialogRemoveLabeler), 180 + content: Text(l10n.dialogRemoveLabelerConfirm), 179 181 actions: [ 180 182 TextButton( 181 183 onPressed: () => Navigator.of(context).pop(false), 182 - child: Text(AppLocalizations.of(context).buttonCancel), 184 + child: Text(l10n.buttonCancel), 183 185 ), 184 186 TextButton( 185 187 onPressed: () => Navigator.of(context).pop(true), 186 - child: const Text('Remove'), 188 + child: Text(l10n.buttonRemove), 187 189 ), 188 190 ], 189 191 ), ··· 204 206 205 207 @override 206 208 Widget build(BuildContext context) { 207 - super.build(context); // Required for AutomaticKeepAliveClientMixin 209 + super.build(context); 208 210 final theme = Theme.of(context); 209 211 final colorScheme = theme.colorScheme; 212 + final l10n = AppLocalizations.of(context); 210 213 211 214 if (_isLoading) { 212 215 return Scaffold( ··· 215 218 backgroundColor: colorScheme.surface, 216 219 elevation: 0, 217 220 leading: const AppLeadingButton(), 218 - title: const Text('Labelers'), 221 + title: Text(l10n.pageTitleLabelers), 219 222 centerTitle: true, 220 223 ), 221 224 body: const Center(child: CircularProgressIndicator()), ··· 228 231 backgroundColor: colorScheme.surface, 229 232 elevation: 0, 230 233 leading: const AppLeadingButton(), 231 - title: const Text('Labelers'), 234 + title: Text(l10n.pageTitleLabelers), 232 235 centerTitle: true, 233 236 ), 234 237 body: RefreshIndicator( 235 238 onRefresh: _loadLabelers, 236 239 child: CustomScrollView( 237 240 slivers: [ 238 - // Description section 239 241 SliverToBoxAdapter( 240 242 child: Padding( 241 243 padding: const EdgeInsets.all(16), 242 244 child: Text( 243 - 'Manage the labelers that provide content moderation ' 244 - 'labels for your feeds.', 245 + l10n.emptyNoLabelersDescription, 245 246 style: TextStyle( 246 247 color: colorScheme.onSurface.withAlpha(178), 247 248 fontSize: 14, ··· 250 251 ), 251 252 ), 252 253 253 - // Action buttons 254 254 SliverToBoxAdapter( 255 255 child: Padding( 256 256 padding: const EdgeInsets.symmetric( ··· 260 260 child: ElevatedButton.icon( 261 261 onPressed: _addLabeler, 262 262 icon: const Icon(Icons.add, size: 18), 263 - label: const Text('Add Labeler'), 263 + label: Text(l10n.labelAddLabeler), 264 264 style: ElevatedButton.styleFrom( 265 265 backgroundColor: colorScheme.primary, 266 266 foregroundColor: colorScheme.onPrimary, ··· 269 269 ), 270 270 ), 271 271 272 - // Labelers list 273 272 if (_labelerDids.isEmpty) 274 273 SliverFillRemaining( 275 274 hasScrollBody: false, ··· 284 283 ), 285 284 const SizedBox(height: 16), 286 285 Text( 287 - 'No Labelers', 286 + l10n.emptyNoLabelers, 288 287 style: TextStyle( 289 288 fontSize: 18, 290 289 fontWeight: FontWeight.bold, ··· 293 292 ), 294 293 const SizedBox(height: 8), 295 294 Text( 296 - 'Add labelers to customize content moderation', 295 + l10n.emptyNoLabelersDescription, 297 296 style: TextStyle( 298 297 color: colorScheme.onSurface.withAlpha(178), 299 298 ), ··· 353 352 LabelerLabelSettingsRoute(did: did), 354 353 ); 355 354 }, 356 - tooltip: 'Label settings', 355 + tooltip: l10n.tooltipLabelSettings, 357 356 ), 358 357 if (!isDefault) 359 358 IconButton( ··· 362 361 icon: const Icon(Icons.delete_outline), 363 362 color: colorScheme.error, 364 363 onPressed: () => _removeLabeler(did), 365 - tooltip: 'Remove labeler', 364 + tooltip: l10n.tooltipRemoveLabeler, 366 365 ), 367 366 ], 368 367 ),
+21 -10
lib/src/features/settings/ui/pages/settings_page.dart
··· 5 5 import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 6 import 'package:get_it/get_it.dart'; 7 7 import 'package:spark/src/core/design_system/components/atoms/buttons/app_leading_button.dart'; 8 + import 'package:spark/src/core/l10n/app_localizations.dart'; 8 9 import 'package:spark/src/core/routing/app_router.dart'; 9 10 import 'package:spark/src/core/utils/logging/log_service.dart'; 10 11 import 'package:spark/src/features/auth/auth.dart'; ··· 189 190 190 191 @override 191 192 Widget build(BuildContext context) { 193 + final l10n = AppLocalizations.of(context); 192 194 return Scaffold( 193 195 backgroundColor: Theme.of(context).colorScheme.surface, 194 196 appBar: AppBar( 195 197 backgroundColor: Theme.of(context).colorScheme.surface, 196 198 elevation: 0, 197 199 leading: const AppLeadingButton(), 198 - title: const Text('Settings'), 200 + title: Text(l10n.pageTitleSettings), 199 201 centerTitle: true, 200 202 ), 201 203 body: ListView( ··· 211 213 borderRadius: BorderRadius.circular(12), 212 214 ), 213 215 child: ListTile( 214 - title: const Text( 215 - 'Feeds', 216 - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), 216 + title: Text( 217 + l10n.pageTitleYourFeeds, 218 + style: const TextStyle( 219 + fontSize: 16, 220 + fontWeight: FontWeight.bold, 221 + ), 217 222 ), 218 223 trailing: const Icon(FluentIcons.list_24_regular), 219 224 onTap: () => context.router.push(const FeedListRoute()), ··· 234 239 borderRadius: BorderRadius.circular(12), 235 240 ), 236 241 child: ListTile( 237 - title: const Text( 238 - 'Labelers', 239 - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), 242 + title: Text( 243 + l10n.pageTitleLabelers, 244 + style: const TextStyle( 245 + fontSize: 16, 246 + fontWeight: FontWeight.bold, 247 + ), 240 248 ), 241 249 trailing: const Icon(FluentIcons.tag_24_regular), 242 250 onTap: () => ··· 281 289 borderRadius: BorderRadius.circular(12), 282 290 ), 283 291 child: ListTile( 284 - title: const Text( 285 - 'Blocked Users', 286 - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), 292 + title: Text( 293 + l10n.pageTitleBlockedUsers, 294 + style: const TextStyle( 295 + fontSize: 16, 296 + fontWeight: FontWeight.bold, 297 + ), 287 298 ), 288 299 trailing: const Icon(FluentIcons.prohibited_24_regular), 289 300 onTap: () => context.router.push(const BlocksRoute()),
+20 -17
lib/src/features/stories/ui/pages/story_manager_page.dart
··· 2 2 import 'package:cached_network_image/cached_network_image.dart'; 3 3 import 'package:flutter/material.dart'; 4 4 import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 + import 'package:spark/src/core/l10n/app_localizations.dart'; 5 6 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 6 7 import 'package:spark/src/core/routing/app_router.dart'; 7 8 import 'package:spark/src/features/stories/providers/story_auto_delete_provider.dart'; ··· 30 31 WidgetRef ref, 31 32 int index, 32 33 ) async { 34 + final l10n = AppLocalizations.of(context); 33 35 final notifier = ref.read(storyManagerProvider.notifier); 34 36 final stories = ref.read(storyManagerProvider).value?.stories ?? []; 35 37 if (index >= stories.length) return; ··· 38 40 await showDialog<bool>( 39 41 context: context, 40 42 builder: (ctx) => AlertDialog( 41 - title: const Text('Delete Story'), 42 - content: const Text('Are you sure you want to delete this story?'), 43 + title: Text(l10n.dialogDeleteStory), 44 + content: Text(l10n.dialogDeleteStoryConfirm), 43 45 actions: [ 44 46 TextButton( 45 47 onPressed: () => Navigator.of(ctx).maybePop(false), 46 - child: const Text('Cancel'), 48 + child: Text(l10n.buttonCancel), 47 49 ), 48 50 TextButton( 49 51 onPressed: () => Navigator.of(ctx).maybePop(true), 50 52 style: TextButton.styleFrom(foregroundColor: Colors.red), 51 - child: const Text('Delete'), 53 + child: Text(l10n.buttonDelete), 52 54 ), 53 55 ], 54 56 ), ··· 62 64 Widget build(BuildContext context, WidgetRef ref) { 63 65 final asyncState = ref.watch(storyManagerProvider); 64 66 final theme = Theme.of(context); 67 + final l10n = AppLocalizations.of(context); 65 68 66 69 final autoDeletePref = ref.watch(storyAutoDeletePrefProvider); 67 70 68 71 return Scaffold( 69 - appBar: AppBar(title: const Text('Story Manager')), 72 + appBar: AppBar(title: Text(l10n.pageTitleStoryManager)), 70 73 body: asyncState.when( 71 74 data: (data) { 72 75 return RefreshIndicator( ··· 74 77 child: ListView.separated( 75 78 padding: const EdgeInsets.all(16), 76 79 separatorBuilder: (_, _) => const SizedBox(height: 12), 77 - itemCount: 1 + data.stories.length, // header + stories 80 + itemCount: 1 + data.stories.length, 78 81 itemBuilder: (ctx, i) { 79 82 if (i == 0) { 80 83 return _AutoDeleteHeader( ··· 132 135 crossAxisAlignment: CrossAxisAlignment.start, 133 136 children: [ 134 137 Text( 135 - 'Story ${data.stories.length - storyIndex}', 138 + l10n.messageStoryNumber( 139 + data.stories.length - storyIndex, 140 + ), 136 141 style: theme.textTheme.titleMedium, 137 142 ), 138 143 const SizedBox(height: 4), 139 144 Text( 140 - 'Posted $ageStr ago', 145 + l10n.messagePostedAgo(ageStr), 141 146 style: theme.textTheme.bodySmall, 142 147 ), 143 148 ], ··· 148 153 Icons.delete_outline, 149 154 color: Colors.red, 150 155 ), 151 - tooltip: 'Delete', 156 + tooltip: l10n.tooltipDelete, 152 157 onPressed: () => 153 158 _deleteStory(context, ref, storyIndex), 154 159 ), ··· 166 171 child: Column( 167 172 mainAxisSize: MainAxisSize.min, 168 173 children: [ 169 - Text('Error: $err'), 174 + Text('${l10n.errorGeneric}: $err'), 170 175 const SizedBox(height: 12), 171 176 ElevatedButton( 172 177 onPressed: () => 173 178 ref.read(storyManagerProvider.notifier).refresh(), 174 - child: const Text('Retry'), 179 + child: Text(l10n.buttonRetry), 175 180 ), 176 181 ], 177 182 ), ··· 189 194 @override 190 195 Widget build(BuildContext context) { 191 196 final theme = Theme.of(context); 197 + final l10n = AppLocalizations.of(context); 192 198 final cardColor = theme.colorScheme.surfaceContainerHighest; 193 199 return Container( 194 200 decoration: BoxDecoration( ··· 206 212 children: [ 207 213 Expanded( 208 214 child: Text( 209 - 'Auto-delete stories', 215 + l10n.messageAutoDeleteStories, 210 216 style: theme.textTheme.titleMedium?.copyWith( 211 217 fontWeight: FontWeight.w600, 212 218 ), ··· 235 241 ), 236 242 error: (_, _) => IconButton( 237 243 icon: const Icon(Icons.refresh), 238 - tooltip: 'Retry', 244 + tooltip: l10n.tooltipRetry, 239 245 onPressed: () => ref.refresh(storyAutoDeletePrefProvider), 240 246 ), 241 247 ), ··· 243 249 ), 244 250 const SizedBox(height: 8), 245 251 Text( 246 - 'Stories are public and stored on your PDS indefinitely. Enable ' 247 - 'this so the app auto deletes them forever after 24h. Enabling ' 248 - 'this will also execute an initial cleanup of any stories older ' 249 - 'than 24h.', 252 + l10n.messageAutoDeleteStoriesDescription, 250 253 style: theme.textTheme.bodySmall?.copyWith( 251 254 color: theme.colorScheme.onSurfaceVariant, 252 255 ),