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 foundation (#28)

* feat: foundational i10n impl

authored by

Owais and committed by
GitHub
e7bf2757 a9fe1f1b

+3110 -253
+97
docs/specs/internationalization.md
··· 1 + --- 2 + title: Internationalization 3 + updated: 2026-05-07 4 + --- 5 + 6 + ## Summary 7 + 8 + Lazurite uses Flutter `gen_l10n` with ARB files and `intl` for user-facing UI 9 + strings. The v1 implementation is English-only, follows the system locale, and 10 + establishes the app-local localization architecture needed for future real 11 + translations. 12 + 13 + ## Current State 14 + 15 + V1 localizes the foundation and first user-facing surface: 16 + 17 + - App bootstrap localization delegates and supported locales 18 + - App shell navigation, drawer, common buttons, dialogs, and error states 19 + - Auth/login copy, saved account actions, and legal links 20 + - Settings sections, common settings rows, provider dialogs, and troubleshooting 21 + - Search tabs, common search placeholders, post filters, and starter-pack API 22 + unavailable messaging 23 + 24 + Text intentionally not localized in v1: 25 + 26 + - User-generated post/profile/list/feed content 27 + - Handles, DIDs, AT URIs, URLs, record JSON, and log lines 28 + - Server/API error details and externally localized moderation label values 29 + - Remaining feature surfaces listed in `docs/tasks/internationalization.md` 30 + 31 + ## Architecture 32 + 33 + Localization source lives in `lib/core/l10n/intl_en.arb`. `l10n.yaml` configures 34 + Flutter generation: 35 + 36 + - `arb-dir: lib/core/l10n` 37 + - `template-arb-file: intl_en.arb` 38 + - `output-localization-file: app_localizations.dart` 39 + - `nullable-getter: false` 40 + - `use-escaping: true` 41 + - `required-resource-attributes: true` 42 + 43 + Generated localization Dart files are checked in because Lazurite imports 44 + `package:lazurite/core/l10n/app_localizations.dart` directly. Widgets should use 45 + `context.l10n` from `package:lazurite/core/l10n/l10n.dart` when they already 46 + have a `BuildContext`; app bootstrap can import `AppLocalizations` directly. 47 + 48 + `MaterialApp.router` owns locale resolution through Flutter's default system 49 + locale behavior. V1 does not add a persisted language setting or runtime picker. 50 + 51 + ## Key Naming Rules 52 + 53 + - Prefer semantic keys over copy-shaped keys: `labelSettings`, not 54 + `settingsText`. 55 + - Use prefixes consistently: 56 + - `button*` for button labels 57 + - `label*` for short labels, titles, tabs, and tooltips 58 + - `message*` for explanatory body text and helper text 59 + - `dialog*` for dialog-specific title/body copy 60 + - `error*` and `validation*` for failure copy 61 + - `format*` for parameterized messages 62 + - Every ARB resource must include metadata because 63 + `required-resource-attributes` is enabled. 64 + - Parameterized strings must use ARB placeholders with `type` and an example. 65 + 66 + ## Formatting Policy 67 + 68 + Use `intl` for locale-sensitive dates and numbers. Context-free helpers in 69 + `shared/utils/format_utils.dart` should accept an optional locale or use 70 + `Intl.getCurrentLocale()` when no `BuildContext` is available. 71 + 72 + Do not localize protocol constants, database keys, enum storage values, route 73 + paths, asset paths, or provider keys. 74 + 75 + ## Testing Policy 76 + 77 + Widget tests covering full localized screens should pump a `MaterialApp` or 78 + `MaterialApp.router` with: 79 + 80 + - `localizationsDelegates: AppLocalizations.localizationsDelegates` 81 + - `supportedLocales: AppLocalizations.supportedLocales` 82 + 83 + The `context.l10n` helper falls back to English for very small widget tests that 84 + intentionally use a bare `MaterialApp`. Tests for English copy should keep 85 + asserting visible English strings for now. When additional locales are added, 86 + add locale-specific widget tests for key flows and locale-sensitive formatting 87 + helpers. 88 + 89 + ## Known Limitations 90 + 91 + - English is the only supported locale in v1. 92 + - There is no in-app language picker. 93 + - Some feature-specific strings remain hard-coded and are tracked as follow-up 94 + milestones. 95 + - Plural, gender, and select messages are only introduced where v1 needs them; 96 + future translation work should revisit social-copy grammar in feed/profile 97 + surfaces.
+40
docs/tasks/internationalization.md
··· 1 + # Internationalization Milestones 2 + 3 + ## M0 - Foundation and English ARB 4 + 5 + - [x] Add `flutter_localizations`, `intl`, `flutter.generate`, and `l10n.yaml` 6 + - [x] Add canonical `lib/core/l10n/intl_en.arb` 7 + - [x] Generate and track `AppLocalizations` Dart files 8 + - [x] Wire `MaterialApp.router` delegates and supported locales 9 + - [x] Add `context.l10n` helper for Lazurite widgets 10 + 11 + ## M1 - Core, Shared, Auth, Settings, Search 12 + 13 + - [x] Localize app shell navigation, drawer labels, and common menu copy 14 + - [x] Localize shared confirmation/error/moderation overlay copy 15 + - [x] Localize login, saved account actions, and legal links 16 + - [x] Localize settings sections, provider dialogs, and troubleshooting actions 17 + - [x] Localize primary search tabs, placeholders, filters, and unavailable states 18 + - [x] Add focused widget/localization tests for migrated surfaces 19 + 20 + ## M2 - Remaining Feature Surfaces 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 27 + 28 + ## M3 - Locale QA and Secondary-Locale Strategy 29 + 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 + - [ ] Decide first real non-English locale and translation ownership 34 + 35 + ## M4 - User-Facing Language Selection 36 + 37 + - [ ] Add persisted language preference only after multiple real locales exist 38 + - [ ] Add settings UI for system/default language and supported overrides 39 + - [ ] Add tests for locale override persistence and app rebuild behavior 40 + - [ ] Document translator workflow and release checklist
+7 -3
justfile
··· 11 11 lint: 12 12 flutter analyze 13 13 14 + # Generate Flutter localization files from ARB sources 15 + l10n: 16 + flutter gen-l10n 17 + 14 18 # Install pinned ObjectBox runtime library for local development 15 19 objectbox-setup: 16 20 bash scripts/objectbox_runtime.sh install ··· 44 48 splash: generate-splash-assets generate-native-splash 45 49 46 50 # Run code gen 47 - gen: generate format 51 + gen: generate l10n format 48 52 49 - # Run format, lint, and test 50 - check: format lint test 53 + # Run localization generation, format, lint, and test 54 + check: l10n format lint test 51 55 52 56 find-comments: 53 57 rg -n --pcre2 '^\s*//(?![!/])' -g '*.dart' -g '!*.g.dart' -g '!*.freezed.dart'
+6
l10n.yaml
··· 1 + arb-dir: lib/core/l10n 2 + template-arb-file: intl_en.arb 3 + output-localization-file: app_localizations.dart 4 + nullable-getter: false 5 + use-escaping: true 6 + required-resource-attributes: true
+1215
lib/core/l10n/app_localizations.dart
··· 1 + import 'dart:async'; 2 + 3 + import 'package:flutter/foundation.dart'; 4 + import 'package:flutter/widgets.dart'; 5 + import 'package:flutter_localizations/flutter_localizations.dart'; 6 + import 'package:intl/intl.dart' as intl; 7 + 8 + import 'app_localizations_en.dart'; 9 + 10 + // ignore_for_file: type=lint 11 + 12 + /// Callers can lookup localized strings with an instance of AppLocalizations 13 + /// returned by `AppLocalizations.of(context)`. 14 + /// 15 + /// Applications need to include `AppLocalizations.delegate()` in their app's 16 + /// `localizationDelegates` list, and the locales they support in the app's 17 + /// `supportedLocales` list. For example: 18 + /// 19 + /// ```dart 20 + /// import 'l10n/app_localizations.dart'; 21 + /// 22 + /// return MaterialApp( 23 + /// localizationsDelegates: AppLocalizations.localizationsDelegates, 24 + /// supportedLocales: AppLocalizations.supportedLocales, 25 + /// home: MyApplicationHome(), 26 + /// ); 27 + /// ``` 28 + /// 29 + /// ## Update pubspec.yaml 30 + /// 31 + /// Please make sure to update your pubspec.yaml to include the following 32 + /// packages: 33 + /// 34 + /// ```yaml 35 + /// dependencies: 36 + /// # Internationalization support. 37 + /// flutter_localizations: 38 + /// sdk: flutter 39 + /// intl: any # Use the pinned version from flutter_localizations 40 + /// 41 + /// # Rest of dependencies 42 + /// ``` 43 + /// 44 + /// ## iOS Applications 45 + /// 46 + /// iOS applications define key application metadata, including supported 47 + /// locales, in an Info.plist file that is built into the application bundle. 48 + /// To configure the locales supported by your app, you’ll need to edit this 49 + /// file. 50 + /// 51 + /// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. 52 + /// Then, in the Project Navigator, open the Info.plist file under the Runner 53 + /// project’s Runner folder. 54 + /// 55 + /// Next, select the Information Property List item, select Add Item from the 56 + /// Editor menu, then select Localizations from the pop-up menu. 57 + /// 58 + /// Select and expand the newly-created Localizations item then, for each 59 + /// locale your application supports, add a new item and select the locale 60 + /// you wish to add from the pop-up menu in the Value field. This list should 61 + /// be consistent with the languages listed in the AppLocalizations.supportedLocales 62 + /// property. 63 + abstract class AppLocalizations { 64 + AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); 65 + 66 + final String localeName; 67 + 68 + static AppLocalizations of(BuildContext context) { 69 + return Localizations.of<AppLocalizations>(context, AppLocalizations)!; 70 + } 71 + 72 + static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate(); 73 + 74 + /// A list of this localizations delegate along with the default localizations 75 + /// delegates. 76 + /// 77 + /// Returns a list of localizations delegates containing this delegate along with 78 + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, 79 + /// and GlobalWidgetsLocalizations.delegate. 80 + /// 81 + /// Additional delegates can be added by appending to this list in 82 + /// MaterialApp. This list does not have to be used at all if a custom list 83 + /// of delegates is preferred or required. 84 + static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[ 85 + delegate, 86 + GlobalMaterialLocalizations.delegate, 87 + GlobalCupertinoLocalizations.delegate, 88 + GlobalWidgetsLocalizations.delegate, 89 + ]; 90 + 91 + /// A list of this localizations delegate's supported locales. 92 + static const List<Locale> supportedLocales = <Locale>[Locale('en')]; 93 + 94 + /// Application title 95 + /// 96 + /// In en, this message translates to: 97 + /// **'Lazurite'** 98 + String get appTitle; 99 + 100 + /// Button label to apply a setting and restart app services 101 + /// 102 + /// In en, this message translates to: 103 + /// **'Apply and Restart'** 104 + String get buttonApplyAndRestart; 105 + 106 + /// Cancel button label 107 + /// 108 + /// In en, this message translates to: 109 + /// **'Cancel'** 110 + String get buttonCancel; 111 + 112 + /// Button label to clear local cache 113 + /// 114 + /// In en, this message translates to: 115 + /// **'Clear Cache'** 116 + String get buttonClearCache; 117 + 118 + /// Continue button label 119 + /// 120 + /// In en, this message translates to: 121 + /// **'Continue'** 122 + String get buttonContinue; 123 + 124 + /// Remove button label 125 + /// 126 + /// In en, this message translates to: 127 + /// **'Remove'** 128 + String get buttonRemove; 129 + 130 + /// Apply button label 131 + /// 132 + /// In en, this message translates to: 133 + /// **'Apply'** 134 + String get buttonApply; 135 + 136 + /// Clear button label 137 + /// 138 + /// In en, this message translates to: 139 + /// **'Clear'** 140 + String get buttonClear; 141 + 142 + /// Clear all filters button label 143 + /// 144 + /// In en, this message translates to: 145 + /// **'Clear all'** 146 + String get buttonClearAll; 147 + 148 + /// Open button label 149 + /// 150 + /// In en, this message translates to: 151 + /// **'Open'** 152 + String get buttonOpen; 153 + 154 + /// Button label to clear local sign-in data 155 + /// 156 + /// In en, this message translates to: 157 + /// **'Reset Sign-In Data'** 158 + String get buttonResetSignInData; 159 + 160 + /// Retry button label 161 + /// 162 + /// In en, this message translates to: 163 + /// **'Retry'** 164 + String get buttonRetry; 165 + 166 + /// Sign in button label 167 + /// 168 + /// In en, this message translates to: 169 + /// **'Sign In'** 170 + String get buttonSignIn; 171 + 172 + /// Button label to reveal moderated content 173 + /// 174 + /// In en, this message translates to: 175 + /// **'Show content'** 176 + String get buttonShowContent; 177 + 178 + /// Try again button label 179 + /// 180 + /// In en, this message translates to: 181 + /// **'Try again'** 182 + String get buttonTryAgain; 183 + 184 + /// Label for a value that has never happened 185 + /// 186 + /// In en, this message translates to: 187 + /// **'Never'** 188 + String get commonNever; 189 + 190 + /// Label for no value 191 + /// 192 + /// In en, this message translates to: 193 + /// **'None'** 194 + String get commonNone; 195 + 196 + /// Label for a health check that has not run 197 + /// 198 + /// In en, this message translates to: 199 + /// **'Not checked yet'** 200 + String get commonNotCheckedYet; 201 + 202 + /// Disabled option label 203 + /// 204 + /// In en, this message translates to: 205 + /// **'Off'** 206 + String get commonOff; 207 + 208 + /// Confirmation dialog body before clearing local cache 209 + /// 210 + /// In en, this message translates to: 211 + /// **'This removes cached posts, profiles, images, feeds, threads, label data, and local semantic search data.\n\nAccounts, settings, drafts, bookmarks, and likes are kept.'** 212 + String get dialogClearCacheContent; 213 + 214 + /// Confirmation dialog title before clearing local cache 215 + /// 216 + /// In en, this message translates to: 217 + /// **'Clear cache?'** 218 + String get dialogClearCacheTitle; 219 + 220 + /// Confirmation dialog body before removing a saved account from this device 221 + /// 222 + /// In en, this message translates to: 223 + /// **'Remove @{handle} from this device?'** 224 + String dialogRemoveAccountContent(String handle); 225 + 226 + /// Confirmation dialog title before removing a saved account 227 + /// 228 + /// In en, this message translates to: 229 + /// **'Remove Account'** 230 + String get dialogRemoveAccountTitle; 231 + 232 + /// Confirmation dialog body before clearing local sign-in data 233 + /// 234 + /// In en, this message translates to: 235 + /// **'Use this only when troubleshooting sign-in or account switching.\n\nThis clears all local account sessions on this device and sends you back to sign in. It does not delete your Bluesky account or posts.'** 236 + String get dialogResetSignInDataContent; 237 + 238 + /// Confirmation dialog title before clearing local sign-in data 239 + /// 240 + /// In en, this message translates to: 241 + /// **'Reset sign-in data?'** 242 + String get dialogResetSignInDataTitle; 243 + 244 + /// Confirmation dialog body before switching AppView provider 245 + /// 246 + /// In en, this message translates to: 247 + /// **'Apply and restart now to rebuild network services.\n\nYou will stay signed in and no local data will be deleted.\n\nModeration labels, ranking, and trending results can differ between providers.'** 248 + String get dialogSwitchAppViewProviderContent; 249 + 250 + /// Confirmation dialog title before switching AppView provider 251 + /// 252 + /// In en, this message translates to: 253 + /// **'Switch AppView provider?'** 254 + String get dialogSwitchAppViewProviderTitle; 255 + 256 + /// Snackbar message when persisting selected provider fails 257 + /// 258 + /// In en, this message translates to: 259 + /// **'Failed to save provider selection: {error}'** 260 + String errorFailedToSaveProviderSelection(Object error); 261 + 262 + /// Snackbar message when cache clearing fails 263 + /// 264 + /// In en, this message translates to: 265 + /// **'Failed to clear cache: {error}'** 266 + String errorFailedToClearCache(Object error); 267 + 268 + /// Generic error state title 269 + /// 270 + /// In en, this message translates to: 271 + /// **'Something went wrong'** 272 + String get errorGenericTitle; 273 + 274 + /// Snackbar message when a saved account cannot be removed 275 + /// 276 + /// In en, this message translates to: 277 + /// **'Unable to remove account right now.'** 278 + String get errorUnableToRemoveAccount; 279 + 280 + /// Tooltip for removing a saved account 281 + /// 282 + /// In en, this message translates to: 283 + /// **'Remove account'** 284 + String get labelRemoveAccount; 285 + 286 + /// Settings account row subtitle when multiple accounts are available 287 + /// 288 + /// In en, this message translates to: 289 + /// **'{count, plural, =1{1 account - tap to switch} other{{count} accounts - tap to switch}}'** 290 + String formatAccountsTapToSwitch(int count); 291 + 292 + /// Settings subtitle for selected AppView provider 293 + /// 294 + /// In en, this message translates to: 295 + /// **'{provider} selected. Switching providers performs a soft restart.'** 296 + String formatAppViewProviderSelected(String provider); 297 + 298 + /// Settings subtitle showing the count of subscribed custom moderation labelers 299 + /// 300 + /// In en, this message translates to: 301 + /// **'{count, plural, =0{No custom labelers subscribed} =1{1 custom labeler subscribed} other{{count} custom labelers subscribed}}'** 302 + String formatContentModerationCustomLabelers(int count); 303 + 304 + /// Thread auto-collapse depth setting option 305 + /// 306 + /// In en, this message translates to: 307 + /// **'Depth {depth}'** 308 + String formatDepth(int depth); 309 + 310 + /// About page or settings label 311 + /// 312 + /// In en, this message translates to: 313 + /// **'About'** 314 + String get labelAbout; 315 + 316 + /// Account settings section label 317 + /// 318 + /// In en, this message translates to: 319 + /// **'Account'** 320 + String get labelAccount; 321 + 322 + /// Settings section for account maintenance actions 323 + /// 324 + /// In en, this message translates to: 325 + /// **'Account Maintenance'** 326 + String get labelAccountMaintenance; 327 + 328 + /// Provider diagnostics label for active provider 329 + /// 330 + /// In en, this message translates to: 331 + /// **'Active Provider'** 332 + String get labelActiveProvider; 333 + 334 + /// Adult content setting label 335 + /// 336 + /// In en, this message translates to: 337 + /// **'Adult Content'** 338 + String get labelAdultContent; 339 + 340 + /// Advanced settings section label 341 + /// 342 + /// In en, this message translates to: 343 + /// **'Advanced'** 344 + String get labelAdvanced; 345 + 346 + /// Bottom navigation alerts label 347 + /// 348 + /// In en, this message translates to: 349 + /// **'ALERTS'** 350 + String get labelAlerts; 351 + 352 + /// Animations settings label 353 + /// 354 + /// In en, this message translates to: 355 + /// **'Animations'** 356 + String get labelAnimations; 357 + 358 + /// Appearance settings section label 359 + /// 360 + /// In en, this message translates to: 361 + /// **'Appearance'** 362 + String get labelAppearance; 363 + 364 + /// Debug app password field label 365 + /// 366 + /// In en, this message translates to: 367 + /// **'App Password'** 368 + String get labelAppPassword; 369 + 370 + /// Debug app password login card title 371 + /// 372 + /// In en, this message translates to: 373 + /// **'App Password Login'** 374 + String get labelAppPasswordLogin; 375 + 376 + /// AppView provider settings label 377 + /// 378 + /// In en, this message translates to: 379 + /// **'AppView Provider'** 380 + String get labelAppViewProvider; 381 + 382 + /// AT Protocol explorer settings/menu label 383 + /// 384 + /// In en, this message translates to: 385 + /// **'AT Explorer'** 386 + String get labelAtExplorer; 387 + 388 + /// Settings section title for AT Protocol connection details 389 + /// 390 + /// In en, this message translates to: 391 + /// **'AT Protocol Connection'** 392 + String get labelAtProtocolConnection; 393 + 394 + /// Menu item for follow audit feature 395 + /// 396 + /// In en, this message translates to: 397 + /// **'Audit Follows'** 398 + String get labelAuditFollows; 399 + 400 + /// Back tooltip label 401 + /// 402 + /// In en, this message translates to: 403 + /// **'Back'** 404 + String get labelBack; 405 + 406 + /// Settings item for bookmarked and liked posts 407 + /// 408 + /// In en, this message translates to: 409 + /// **'Bookmarks & Likes'** 410 + String get labelBookmarksAndLikes; 411 + 412 + /// Snackbar message after clearing cache 413 + /// 414 + /// In en, this message translates to: 415 + /// **'Cache cleared'** 416 + String get labelCacheCleared; 417 + 418 + /// Login provider selection label 419 + /// 420 + /// In en, this message translates to: 421 + /// **'Choose your portal'** 422 + String get labelChooseYourPortal; 423 + 424 + /// Settings item for follow audit feature 425 + /// 426 + /// In en, this message translates to: 427 + /// **'Clean Follows'** 428 + String get labelCleanFollows; 429 + 430 + /// Settings item for clearing cache 431 + /// 432 + /// In en, this message translates to: 433 + /// **'Clear Cache'** 434 + String get labelClearCache; 435 + 436 + /// Community provider option label 437 + /// 438 + /// In en, this message translates to: 439 + /// **'Community'** 440 + String get labelCommunity; 441 + 442 + /// Constellation URL setting label 443 + /// 444 + /// In en, this message translates to: 445 + /// **'Constellation URL'** 446 + String get labelConstellationUrl; 447 + 448 + /// Content moderation settings label 449 + /// 450 + /// In en, this message translates to: 451 + /// **'Content Moderation'** 452 + String get labelContentModeration; 453 + 454 + /// Semantic label for continuing sign in 455 + /// 456 + /// In en, this message translates to: 457 + /// **'Continue sign in'** 458 + String get labelContinueSignIn; 459 + 460 + /// Crash reporting setting label 461 + /// 462 + /// In en, this message translates to: 463 + /// **'Crash Reporting'** 464 + String get labelCrashReporting; 465 + 466 + /// Developer setting to trigger a test crash 467 + /// 468 + /// In en, this message translates to: 469 + /// **'Crashlytics Test Crash'** 470 + String get labelCrashlyticsTestCrash; 471 + 472 + /// Cross-provider fallback setting label 473 + /// 474 + /// In en, this message translates to: 475 + /// **'Cross-Provider Fallback'** 476 + String get labelCrossProviderFallback; 477 + 478 + /// Destructive settings section label 479 + /// 480 + /// In en, this message translates to: 481 + /// **'Danger Zone'** 482 + String get labelDangerZone; 483 + 484 + /// Dark theme option label 485 + /// 486 + /// In en, this message translates to: 487 + /// **'Dark'** 488 + String get labelDark; 489 + 490 + /// Debug section label 491 + /// 492 + /// In en, this message translates to: 493 + /// **'Debug'** 494 + String get labelDebug; 495 + 496 + /// Developer settings section label 497 + /// 498 + /// In en, this message translates to: 499 + /// **'Developer'** 500 + String get labelDeveloper; 501 + 502 + /// Feed layout setting label 503 + /// 504 + /// In en, this message translates to: 505 + /// **'Feed Layout'** 506 + String get labelFeedLayout; 507 + 508 + /// Feeds menu/settings label 509 + /// 510 + /// In en, this message translates to: 511 + /// **'Feeds'** 512 + String get labelFeeds; 513 + 514 + /// Developer setting to force the next XRPC request to return Unauthorized 515 + /// 516 + /// In en, this message translates to: 517 + /// **'Force Next XRPC 401'** 518 + String get labelForceNextXrpc401; 519 + 520 + /// Developer setting to simulate offline mode 521 + /// 522 + /// In en, this message translates to: 523 + /// **'Go Offline'** 524 + String get labelGoOffline; 525 + 526 + /// Fallback account display name when no user is signed in 527 + /// 528 + /// In en, this message translates to: 529 + /// **'Guest'** 530 + String get labelGuest; 531 + 532 + /// Provider diagnostics health label 533 + /// 534 + /// In en, this message translates to: 535 + /// **'Health'** 536 + String get labelHealth; 537 + 538 + /// Handle field label 539 + /// 540 + /// In en, this message translates to: 541 + /// **'Handle'** 542 + String get labelHandle; 543 + 544 + /// Hide button label 545 + /// 546 + /// In en, this message translates to: 547 + /// **'Hide'** 548 + String get labelHide; 549 + 550 + /// Bottom navigation home label 551 + /// 552 + /// In en, this message translates to: 553 + /// **'HOME'** 554 + String get labelHome; 555 + 556 + /// Provider diagnostics last error label 557 + /// 558 + /// In en, this message translates to: 559 + /// **'Last Error'** 560 + String get labelLastError; 561 + 562 + /// Provider diagnostics last fallback label 563 + /// 564 + /// In en, this message translates to: 565 + /// **'Last Fallback'** 566 + String get labelLastFallback; 567 + 568 + /// Provider diagnostics last health check label 569 + /// 570 + /// In en, this message translates to: 571 + /// **'Last Health Check'** 572 + String get labelLastHealthCheck; 573 + 574 + /// Layout settings section label 575 + /// 576 + /// In en, this message translates to: 577 + /// **'Layout'** 578 + String get labelLayout; 579 + 580 + /// Light theme option label 581 + /// 582 + /// In en, this message translates to: 583 + /// **'Light'** 584 + String get labelLight; 585 + 586 + /// Log out button or menu label 587 + /// 588 + /// In en, this message translates to: 589 + /// **'Log Out'** 590 + String get labelLogOut; 591 + 592 + /// Logs settings label 593 + /// 594 + /// In en, this message translates to: 595 + /// **'Logs'** 596 + String get labelLogs; 597 + 598 + /// Messages menu label 599 + /// 600 + /// In en, this message translates to: 601 + /// **'Messages'** 602 + String get labelMessages; 603 + 604 + /// Moderation settings section label 605 + /// 606 + /// In en, this message translates to: 607 + /// **'Moderation'** 608 + String get labelModeration; 609 + 610 + /// Navigation menu section label 611 + /// 612 + /// In en, this message translates to: 613 + /// **'Navigation'** 614 + String get labelNavigation; 615 + 616 + /// New post menu label 617 + /// 618 + /// In en, this message translates to: 619 + /// **'New Post'** 620 + String get labelNewPost; 621 + 622 + /// Notifications menu label 623 + /// 624 + /// In en, this message translates to: 625 + /// **'Notifications'** 626 + String get labelNotifications; 627 + 628 + /// Tooltip for opening the navigation menu 629 + /// 630 + /// In en, this message translates to: 631 + /// **'Open menu'** 632 + String get labelOpenMenu; 633 + 634 + /// Privacy policy link label 635 + /// 636 + /// In en, this message translates to: 637 + /// **'Privacy Policy'** 638 + String get labelPrivacyPolicy; 639 + 640 + /// Bottom navigation profile label 641 + /// 642 + /// In en, this message translates to: 643 + /// **'PROFILE'** 644 + String get labelProfile; 645 + 646 + /// Settings section for provider diagnostics 647 + /// 648 + /// In en, this message translates to: 649 + /// **'Provider Diagnostics'** 650 + String get labelProviderDiagnostics; 651 + 652 + /// Settings item for refreshing provider health 653 + /// 654 + /// In en, this message translates to: 655 + /// **'Refresh Provider Health'** 656 + String get labelRefreshProviderHealth; 657 + 658 + /// Settings item for resetting local sign-in data 659 + /// 660 + /// In en, this message translates to: 661 + /// **'Reset Sign-In Data'** 662 + String get labelResetSignInData; 663 + 664 + /// Login screen subtitle 665 + /// 666 + /// In en, this message translates to: 667 + /// **'Roam the ATmosphere'** 668 + String get labelRoamTheAtmosphere; 669 + 670 + /// Search menu/settings label 671 + /// 672 + /// In en, this message translates to: 673 + /// **'Search'** 674 + String get labelSearch; 675 + 676 + /// Title for posts-only search mode 677 + /// 678 + /// In en, this message translates to: 679 + /// **'Search Posts'** 680 + String get labelSearchPosts; 681 + 682 + /// Button and dialog title for jumping directly to a profile 683 + /// 684 + /// In en, this message translates to: 685 + /// **'Jump to profile'** 686 + String get labelJumpToProfile; 687 + 688 + /// Posts tab label 689 + /// 690 + /// In en, this message translates to: 691 + /// **'Posts'** 692 + String get labelPosts; 693 + 694 + /// People tab label 695 + /// 696 + /// In en, this message translates to: 697 + /// **'People'** 698 + String get labelPeople; 699 + 700 + /// Starter packs tab label 701 + /// 702 + /// In en, this message translates to: 703 + /// **'Starter Packs'** 704 + String get labelStarterPacks; 705 + 706 + /// Sort control label 707 + /// 708 + /// In en, this message translates to: 709 + /// **'Sort by'** 710 + String get labelSortBy; 711 + 712 + /// Top sort option label 713 + /// 714 + /// In en, this message translates to: 715 + /// **'Top'** 716 + String get labelTop; 717 + 718 + /// Latest sort option label 719 + /// 720 + /// In en, this message translates to: 721 + /// **'Latest'** 722 + String get labelLatest; 723 + 724 + /// Search filters button label 725 + /// 726 + /// In en, this message translates to: 727 + /// **'Filters'** 728 + String get labelFilters; 729 + 730 + /// Post filters sheet title 731 + /// 732 + /// In en, this message translates to: 733 + /// **'Post filters'** 734 + String get labelPostFilters; 735 + 736 + /// Mentions filter label 737 + /// 738 + /// In en, this message translates to: 739 + /// **'Mentions'** 740 + String get labelMentions; 741 + 742 + /// Author filter label 743 + /// 744 + /// In en, this message translates to: 745 + /// **'Author'** 746 + String get labelAuthor; 747 + 748 + /// Fixed author filter label 749 + /// 750 + /// In en, this message translates to: 751 + /// **'Author (fixed)'** 752 + String get labelAuthorFixed; 753 + 754 + /// Language filter label 755 + /// 756 + /// In en, this message translates to: 757 + /// **'Language'** 758 + String get labelLanguage; 759 + 760 + /// Domain filter label 761 + /// 762 + /// In en, this message translates to: 763 + /// **'Domain'** 764 + String get labelDomain; 765 + 766 + /// URL filter label 767 + /// 768 + /// In en, this message translates to: 769 + /// **'URL'** 770 + String get labelUrl; 771 + 772 + /// Tags filter label 773 + /// 774 + /// In en, this message translates to: 775 + /// **'Tags'** 776 + String get labelTags; 777 + 778 + /// Since date filter label 779 + /// 780 + /// In en, this message translates to: 781 + /// **'Since'** 782 + String get labelSince; 783 + 784 + /// Until date filter label 785 + /// 786 + /// In en, this message translates to: 787 + /// **'Until'** 788 + String get labelUntil; 789 + 790 + /// Button label to clear since date filter 791 + /// 792 + /// In en, this message translates to: 793 + /// **'Clear since'** 794 + String get labelClearSince; 795 + 796 + /// Button label to clear until date filter 797 + /// 798 + /// In en, this message translates to: 799 + /// **'Clear until'** 800 + String get labelClearUntil; 801 + 802 + /// Saved accounts section label on login screen 803 + /// 804 + /// In en, this message translates to: 805 + /// **'Saved accounts'** 806 + String get labelSavedAccounts; 807 + 808 + /// Bottom navigation search label 809 + /// 810 + /// In en, this message translates to: 811 + /// **'SEARCH'** 812 + String get labelSearchNav; 813 + 814 + /// Semantic search settings label 815 + /// 816 + /// In en, this message translates to: 817 + /// **'Semantic Search'** 818 + String get labelSemanticSearch; 819 + 820 + /// Settings page title or tooltip 821 + /// 822 + /// In en, this message translates to: 823 + /// **'Settings'** 824 + String get labelSettings; 825 + 826 + /// Show button label 827 + /// 828 + /// In en, this message translates to: 829 + /// **'Show'** 830 + String get labelShow; 831 + 832 + /// Fallback account handle text when sign in is required 833 + /// 834 + /// In en, this message translates to: 835 + /// **'Sign in required'** 836 + String get labelSignInRequired; 837 + 838 + /// Slingshot identity fallback setting label 839 + /// 840 + /// In en, this message translates to: 841 + /// **'Slingshot Identity Fallback'** 842 + String get labelSlingshotIdentityFallback; 843 + 844 + /// Tooltip while sign in is starting 845 + /// 846 + /// In en, this message translates to: 847 + /// **'Starting sign in'** 848 + String get labelStartingSignIn; 849 + 850 + /// System theme option label 851 + /// 852 + /// In en, this message translates to: 853 + /// **'System'** 854 + String get labelSystem; 855 + 856 + /// Terms of service link label 857 + /// 858 + /// In en, this message translates to: 859 + /// **'Terms of Service'** 860 + String get labelTermsOfService; 861 + 862 + /// Theme subsection label in settings 863 + /// 864 + /// In en, this message translates to: 865 + /// **'THEME'** 866 + String get labelTheme; 867 + 868 + /// Thread auto-collapse setting label 869 + /// 870 + /// In en, this message translates to: 871 + /// **'Thread Auto-Collapse'** 872 + String get labelThreadAutoCollapse; 873 + 874 + /// Troubleshooting settings section label 875 + /// 876 + /// In en, this message translates to: 877 + /// **'Troubleshooting'** 878 + String get labelTroubleshooting; 879 + 880 + /// Typeahead provider settings label 881 + /// 882 + /// In en, this message translates to: 883 + /// **'Typeahead Provider'** 884 + String get labelTypeaheadProvider; 885 + 886 + /// Video upload limits settings label 887 + /// 888 + /// In en, this message translates to: 889 + /// **'Video Upload Limits'** 890 + String get labelVideoUploadLimits; 891 + 892 + /// Debug app password help text 893 + /// 894 + /// In en, this message translates to: 895 + /// **'Can be generated via Bluesky\'\'s App Passwords section at bsky.app.'** 896 + String get messageAppPasswordGeneratedViaBluesky; 897 + 898 + /// Snackbar message after arming debug unauthorized response 899 + /// 900 + /// In en, this message translates to: 901 + /// **'Armed: next XRPC request will return debug 401 Unauthorized'** 902 + String get messageAppViewDebug401Armed; 903 + 904 + /// Settings subtitle for Bluesky typeahead provider 905 + /// 906 + /// In en, this message translates to: 907 + /// **'Bluesky official endpoint selected.'** 908 + String get messageBlueskyEndpointSelected; 909 + 910 + /// Settings subtitle for bookmarks and likes 911 + /// 912 + /// In en, this message translates to: 913 + /// **'View your bookmarked and liked posts'** 914 + String get messageBookmarksAndLikesSubtitle; 915 + 916 + /// Login screen provider selection helper text 917 + /// 918 + /// In en, this message translates to: 919 + /// **'Choose the AppView provider used for sign in and public reads.'** 920 + String get messageChooseProviderSubtitle; 921 + 922 + /// Settings subtitle for clean follows 923 + /// 924 + /// In en, this message translates to: 925 + /// **'Audit and unfollow problematic accounts in bulk'** 926 + String get messageCleanFollowsSubtitle; 927 + 928 + /// Settings subtitle for clearing cache 929 + /// 930 + /// In en, this message translates to: 931 + /// **'Remove cached posts, profiles, images, feeds, threads, and semantic search data'** 932 + String get messageClearCacheSubtitle; 933 + 934 + /// Settings subtitle for community typeahead provider 935 + /// 936 + /// In en, this message translates to: 937 + /// **'Community (waow.tech) selected. Third-party service.'** 938 + String get messageCommunityTypeaheadSelected; 939 + 940 + /// Settings subtitle for content moderation 941 + /// 942 + /// In en, this message translates to: 943 + /// **'Manage labelers and visibility rules'** 944 + String get messageContentModerationSubtitle; 945 + 946 + /// Settings subtitle when adult content preference is enabled 947 + /// 948 + /// In en, this message translates to: 949 + /// **'18+ labels can be configured'** 950 + String get messageAdultContentEnabled; 951 + 952 + /// Settings subtitle when adult content preference is disabled 953 + /// 954 + /// In en, this message translates to: 955 + /// **'Required before 18+ labels can be configured'** 956 + String get messageAdultContentRequired; 957 + 958 + /// Settings subtitle when crash reporting is disabled 959 + /// 960 + /// In en, this message translates to: 961 + /// **'Disabled. Crash and error reports are not sent.'** 962 + String get messageCrashReportingDisabled; 963 + 964 + /// Settings subtitle when crash reporting is enabled 965 + /// 966 + /// In en, this message translates to: 967 + /// **'Enabled. Crash and error reports are sent to improve stability.'** 968 + String get messageCrashReportingEnabled; 969 + 970 + /// Developer setting subtitle for test crash 971 + /// 972 + /// In en, this message translates to: 973 + /// **'Intentionally crash to validate Crashlytics reports'** 974 + String get messageCrashlyticsTestCrashSubtitle; 975 + 976 + /// Settings subtitle for cross-provider fallback 977 + /// 978 + /// In en, this message translates to: 979 + /// **'Retry public reads on the alternate AppView when transient errors occur'** 980 + String get messageCrossProviderFallbackSubtitle; 981 + 982 + /// Developer setting subtitle for simulated offline 983 + /// 984 + /// In en, this message translates to: 985 + /// **'Turn off online connectivity'** 986 + String get messageDeveloperGoOfflineSubtitle; 987 + 988 + /// Feed layout card option 989 + /// 990 + /// In en, this message translates to: 991 + /// **'Card'** 992 + String get messageFeedLayoutCard; 993 + 994 + /// Loading message while saved accounts load 995 + /// 996 + /// In en, this message translates to: 997 + /// **'Loading saved accounts...'** 998 + String get messageLoadingSavedAccounts; 999 + 1000 + /// Feed layout compact option 1001 + /// 1002 + /// In en, this message translates to: 1003 + /// **'Compact'** 1004 + String get messageFeedLayoutCompact; 1005 + 1006 + /// Settings subtitle for feeds 1007 + /// 1008 + /// In en, this message translates to: 1009 + /// **'Manage pinned and saved feeds'** 1010 + String get messageFeedsSubtitle; 1011 + 1012 + /// Developer setting subtitle for forced 401 1013 + /// 1014 + /// In en, this message translates to: 1015 + /// **'Debug-only: next network request returns Unauthorized to test token refresh'** 1016 + String get messageForceNextXrpc401Subtitle; 1017 + 1018 + /// Settings subtitle for semantic search management 1019 + /// 1020 + /// In en, this message translates to: 1021 + /// **'Manage semantic search from Bookmarks & Likes -> Search'** 1022 + String get messageManageSemanticSearchSubtitle; 1023 + 1024 + /// Moderation overlay description when content cannot be revealed 1025 + /// 1026 + /// In en, this message translates to: 1027 + /// **'Hidden by your moderation settings and cannot be revealed here.'** 1028 + String get messageModeratedContentCannotReveal; 1029 + 1030 + /// Moderation overlay description when content can be revealed 1031 + /// 1032 + /// In en, this message translates to: 1033 + /// **'Hidden by your moderation settings. You can reveal it for this view.'** 1034 + String get messageModeratedContentCanReveal; 1035 + 1036 + /// Settings subtitle for provider diagnostics 1037 + /// 1038 + /// In en, this message translates to: 1039 + /// **'Moderation/ranking can differ by provider. Verify health and recent fallback state.'** 1040 + String get messageProviderDiagnosticsSubtitle; 1041 + 1042 + /// Settings subtitle for refreshing provider health 1043 + /// 1044 + /// In en, this message translates to: 1045 + /// **'Probe public AppView endpoints now'** 1046 + String get messageRefreshProviderHealthSubtitle; 1047 + 1048 + /// Settings subtitle for resetting sign-in data 1049 + /// 1050 + /// In en, this message translates to: 1051 + /// **'Troubleshoot OAuth or account-switching issues by clearing local sessions on this device'** 1052 + String get messageResetSignInDataSubtitle; 1053 + 1054 + /// Generic search subtitle 1055 + /// 1056 + /// In en, this message translates to: 1057 + /// **'Search'** 1058 + String get messageSearchSubtitle; 1059 + 1060 + /// Confirmation dialog body before clearing search history 1061 + /// 1062 + /// In en, this message translates to: 1063 + /// **'This will delete all your recent searches.'** 1064 + String get messageClearSearchHistoryContent; 1065 + 1066 + /// Confirmation dialog title before clearing search history 1067 + /// 1068 + /// In en, this message translates to: 1069 + /// **'Clear search history?'** 1070 + String get messageClearSearchHistoryTitle; 1071 + 1072 + /// Hint shown in jump to profile dialog 1073 + /// 1074 + /// In en, this message translates to: 1075 + /// **'Start typing to search handles.'** 1076 + String get messageStartTypingToSearchHandles; 1077 + 1078 + /// Helper text when starter pack search is disabled 1079 + /// 1080 + /// In en, this message translates to: 1081 + /// **'Starter pack search is not available in the API yet.'** 1082 + String get messageStarterPackSearchApiUnavailable; 1083 + 1084 + /// Starter pack search unavailable state title 1085 + /// 1086 + /// In en, this message translates to: 1087 + /// **'Starter Pack Search Is Unavailable'** 1088 + String get messageStarterPackSearchUnavailableTitle; 1089 + 1090 + /// Starter pack search unavailable state body 1091 + /// 1092 + /// In en, this message translates to: 1093 + /// **'(Starter Pack Search is not yet implemented in the BlueSky API)'** 1094 + String get messageStarterPackSearchUnavailableBody; 1095 + 1096 + /// Button label to open upstream API issue 1097 + /// 1098 + /// In en, this message translates to: 1099 + /// **'Track API progress'** 1100 + String get messageTrackApiProgress; 1101 + 1102 + /// Snackbar message when opening upstream issue link fails 1103 + /// 1104 + /// In en, this message translates to: 1105 + /// **'Could not open issue link.'** 1106 + String get messageCouldNotOpenIssueLink; 1107 + 1108 + /// Search posts field placeholder 1109 + /// 1110 + /// In en, this message translates to: 1111 + /// **'Search posts'** 1112 + String get messageSearchPostsPlaceholder; 1113 + 1114 + /// Search placeholder in profile posts-only mode 1115 + /// 1116 + /// In en, this message translates to: 1117 + /// **'Search this profile\'\'s posts'** 1118 + String get messageSearchThisProfilesPostsPlaceholder; 1119 + 1120 + /// Search people field placeholder 1121 + /// 1122 + /// In en, this message translates to: 1123 + /// **'Search people'** 1124 + String get messageSearchPeoplePlaceholder; 1125 + 1126 + /// Search feeds field placeholder 1127 + /// 1128 + /// In en, this message translates to: 1129 + /// **'Search feeds'** 1130 + String get messageSearchFeedsPlaceholder; 1131 + 1132 + /// Starter pack search field placeholder 1133 + /// 1134 + /// In en, this message translates to: 1135 + /// **'Starter pack search unavailable'** 1136 + String get messageStarterPackSearchUnavailablePlaceholder; 1137 + 1138 + /// Settings subtitle for Slingshot identity fallback 1139 + /// 1140 + /// In en, this message translates to: 1141 + /// **'If handle lookup fails, use Slingshot to find your DID and PDS so sign-in can continue'** 1142 + String get messageSlingshotIdentityFallbackSubtitle; 1143 + 1144 + /// Settings subtitle for thread auto-collapse 1145 + /// 1146 + /// In en, this message translates to: 1147 + /// **'Collapse reply branches deeper than the selected level'** 1148 + String get messageThreadAutoCollapseSubtitle; 1149 + 1150 + /// Settings subtitle for animations 1151 + /// 1152 + /// In en, this message translates to: 1153 + /// **'Turn off non-essential motion effects'** 1154 + String get messageTurnOffNonEssentialMotion; 1155 + 1156 + /// Settings subtitle for video upload limits 1157 + /// 1158 + /// In en, this message translates to: 1159 + /// **'Check your daily video quota'** 1160 + String get messageVideoUploadLimitsSubtitle; 1161 + 1162 + /// Placeholder for app password field 1163 + /// 1164 + /// In en, this message translates to: 1165 + /// **'xxxx-xxxx-xxxx-xxxx'** 1166 + String get placeholderAppPassword; 1167 + 1168 + /// Placeholder for handle or DID sign-in field 1169 + /// 1170 + /// In en, this message translates to: 1171 + /// **'username.bsky.social or did:plc:...'** 1172 + String get placeholderHandleOrDid; 1173 + 1174 + /// Label for handle or DID sign-in field 1175 + /// 1176 + /// In en, this message translates to: 1177 + /// **'Handle or DID'** 1178 + String get promptHandleOrDid; 1179 + 1180 + /// Validation error for missing app password 1181 + /// 1182 + /// In en, this message translates to: 1183 + /// **'Enter your app password'** 1184 + String get validationEnterAppPassword; 1185 + } 1186 + 1187 + class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> { 1188 + const _AppLocalizationsDelegate(); 1189 + 1190 + @override 1191 + Future<AppLocalizations> load(Locale locale) { 1192 + return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale)); 1193 + } 1194 + 1195 + @override 1196 + bool isSupported(Locale locale) => <String>['en'].contains(locale.languageCode); 1197 + 1198 + @override 1199 + bool shouldReload(_AppLocalizationsDelegate old) => false; 1200 + } 1201 + 1202 + AppLocalizations lookupAppLocalizations(Locale locale) { 1203 + // Lookup logic when only language code is specified. 1204 + switch (locale.languageCode) { 1205 + case 'en': 1206 + return AppLocalizationsEn(); 1207 + } 1208 + 1209 + throw FlutterError( 1210 + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' 1211 + 'an issue with the localizations generation tool. Please file an issue ' 1212 + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' 1213 + 'that was used.', 1214 + ); 1215 + }
+594
lib/core/l10n/app_localizations_en.dart
··· 1 + // ignore: unused_import 2 + import 'package:intl/intl.dart' as intl; 3 + import 'app_localizations.dart'; 4 + 5 + // ignore_for_file: type=lint 6 + 7 + /// The translations for English (`en`). 8 + class AppLocalizationsEn extends AppLocalizations { 9 + AppLocalizationsEn([String locale = 'en']) : super(locale); 10 + 11 + @override 12 + String get appTitle => 'Lazurite'; 13 + 14 + @override 15 + String get buttonApplyAndRestart => 'Apply and Restart'; 16 + 17 + @override 18 + String get buttonCancel => 'Cancel'; 19 + 20 + @override 21 + String get buttonClearCache => 'Clear Cache'; 22 + 23 + @override 24 + String get buttonContinue => 'Continue'; 25 + 26 + @override 27 + String get buttonRemove => 'Remove'; 28 + 29 + @override 30 + String get buttonApply => 'Apply'; 31 + 32 + @override 33 + String get buttonClear => 'Clear'; 34 + 35 + @override 36 + String get buttonClearAll => 'Clear all'; 37 + 38 + @override 39 + String get buttonOpen => 'Open'; 40 + 41 + @override 42 + String get buttonResetSignInData => 'Reset Sign-In Data'; 43 + 44 + @override 45 + String get buttonRetry => 'Retry'; 46 + 47 + @override 48 + String get buttonSignIn => 'Sign In'; 49 + 50 + @override 51 + String get buttonShowContent => 'Show content'; 52 + 53 + @override 54 + String get buttonTryAgain => 'Try again'; 55 + 56 + @override 57 + String get commonNever => 'Never'; 58 + 59 + @override 60 + String get commonNone => 'None'; 61 + 62 + @override 63 + String get commonNotCheckedYet => 'Not checked yet'; 64 + 65 + @override 66 + String get commonOff => 'Off'; 67 + 68 + @override 69 + String get dialogClearCacheContent => 70 + 'This removes cached posts, profiles, images, feeds, threads, label data, and local semantic search data.\n\nAccounts, settings, drafts, bookmarks, and likes are kept.'; 71 + 72 + @override 73 + String get dialogClearCacheTitle => 'Clear cache?'; 74 + 75 + @override 76 + String dialogRemoveAccountContent(String handle) { 77 + return 'Remove @$handle from this device?'; 78 + } 79 + 80 + @override 81 + String get dialogRemoveAccountTitle => 'Remove Account'; 82 + 83 + @override 84 + String get dialogResetSignInDataContent => 85 + 'Use this only when troubleshooting sign-in or account switching.\n\nThis clears all local account sessions on this device and sends you back to sign in. It does not delete your Bluesky account or posts.'; 86 + 87 + @override 88 + String get dialogResetSignInDataTitle => 'Reset sign-in data?'; 89 + 90 + @override 91 + String get dialogSwitchAppViewProviderContent => 92 + 'Apply and restart now to rebuild network services.\n\nYou will stay signed in and no local data will be deleted.\n\nModeration labels, ranking, and trending results can differ between providers.'; 93 + 94 + @override 95 + String get dialogSwitchAppViewProviderTitle => 'Switch AppView provider?'; 96 + 97 + @override 98 + String errorFailedToSaveProviderSelection(Object error) { 99 + return 'Failed to save provider selection: $error'; 100 + } 101 + 102 + @override 103 + String errorFailedToClearCache(Object error) { 104 + return 'Failed to clear cache: $error'; 105 + } 106 + 107 + @override 108 + String get errorGenericTitle => 'Something went wrong'; 109 + 110 + @override 111 + String get errorUnableToRemoveAccount => 'Unable to remove account right now.'; 112 + 113 + @override 114 + String get labelRemoveAccount => 'Remove account'; 115 + 116 + @override 117 + String formatAccountsTapToSwitch(int count) { 118 + String _temp0 = intl.Intl.pluralLogic( 119 + count, 120 + locale: localeName, 121 + other: '$count accounts - tap to switch', 122 + one: '1 account - tap to switch', 123 + ); 124 + return '$_temp0'; 125 + } 126 + 127 + @override 128 + String formatAppViewProviderSelected(String provider) { 129 + return '$provider selected. Switching providers performs a soft restart.'; 130 + } 131 + 132 + @override 133 + String formatContentModerationCustomLabelers(int count) { 134 + String _temp0 = intl.Intl.pluralLogic( 135 + count, 136 + locale: localeName, 137 + other: '$count custom labelers subscribed', 138 + one: '1 custom labeler subscribed', 139 + zero: 'No custom labelers subscribed', 140 + ); 141 + return '$_temp0'; 142 + } 143 + 144 + @override 145 + String formatDepth(int depth) { 146 + return 'Depth $depth'; 147 + } 148 + 149 + @override 150 + String get labelAbout => 'About'; 151 + 152 + @override 153 + String get labelAccount => 'Account'; 154 + 155 + @override 156 + String get labelAccountMaintenance => 'Account Maintenance'; 157 + 158 + @override 159 + String get labelActiveProvider => 'Active Provider'; 160 + 161 + @override 162 + String get labelAdultContent => 'Adult Content'; 163 + 164 + @override 165 + String get labelAdvanced => 'Advanced'; 166 + 167 + @override 168 + String get labelAlerts => 'ALERTS'; 169 + 170 + @override 171 + String get labelAnimations => 'Animations'; 172 + 173 + @override 174 + String get labelAppearance => 'Appearance'; 175 + 176 + @override 177 + String get labelAppPassword => 'App Password'; 178 + 179 + @override 180 + String get labelAppPasswordLogin => 'App Password Login'; 181 + 182 + @override 183 + String get labelAppViewProvider => 'AppView Provider'; 184 + 185 + @override 186 + String get labelAtExplorer => 'AT Explorer'; 187 + 188 + @override 189 + String get labelAtProtocolConnection => 'AT Protocol Connection'; 190 + 191 + @override 192 + String get labelAuditFollows => 'Audit Follows'; 193 + 194 + @override 195 + String get labelBack => 'Back'; 196 + 197 + @override 198 + String get labelBookmarksAndLikes => 'Bookmarks & Likes'; 199 + 200 + @override 201 + String get labelCacheCleared => 'Cache cleared'; 202 + 203 + @override 204 + String get labelChooseYourPortal => 'Choose your portal'; 205 + 206 + @override 207 + String get labelCleanFollows => 'Clean Follows'; 208 + 209 + @override 210 + String get labelClearCache => 'Clear Cache'; 211 + 212 + @override 213 + String get labelCommunity => 'Community'; 214 + 215 + @override 216 + String get labelConstellationUrl => 'Constellation URL'; 217 + 218 + @override 219 + String get labelContentModeration => 'Content Moderation'; 220 + 221 + @override 222 + String get labelContinueSignIn => 'Continue sign in'; 223 + 224 + @override 225 + String get labelCrashReporting => 'Crash Reporting'; 226 + 227 + @override 228 + String get labelCrashlyticsTestCrash => 'Crashlytics Test Crash'; 229 + 230 + @override 231 + String get labelCrossProviderFallback => 'Cross-Provider Fallback'; 232 + 233 + @override 234 + String get labelDangerZone => 'Danger Zone'; 235 + 236 + @override 237 + String get labelDark => 'Dark'; 238 + 239 + @override 240 + String get labelDebug => 'Debug'; 241 + 242 + @override 243 + String get labelDeveloper => 'Developer'; 244 + 245 + @override 246 + String get labelFeedLayout => 'Feed Layout'; 247 + 248 + @override 249 + String get labelFeeds => 'Feeds'; 250 + 251 + @override 252 + String get labelForceNextXrpc401 => 'Force Next XRPC 401'; 253 + 254 + @override 255 + String get labelGoOffline => 'Go Offline'; 256 + 257 + @override 258 + String get labelGuest => 'Guest'; 259 + 260 + @override 261 + String get labelHealth => 'Health'; 262 + 263 + @override 264 + String get labelHandle => 'Handle'; 265 + 266 + @override 267 + String get labelHide => 'Hide'; 268 + 269 + @override 270 + String get labelHome => 'HOME'; 271 + 272 + @override 273 + String get labelLastError => 'Last Error'; 274 + 275 + @override 276 + String get labelLastFallback => 'Last Fallback'; 277 + 278 + @override 279 + String get labelLastHealthCheck => 'Last Health Check'; 280 + 281 + @override 282 + String get labelLayout => 'Layout'; 283 + 284 + @override 285 + String get labelLight => 'Light'; 286 + 287 + @override 288 + String get labelLogOut => 'Log Out'; 289 + 290 + @override 291 + String get labelLogs => 'Logs'; 292 + 293 + @override 294 + String get labelMessages => 'Messages'; 295 + 296 + @override 297 + String get labelModeration => 'Moderation'; 298 + 299 + @override 300 + String get labelNavigation => 'Navigation'; 301 + 302 + @override 303 + String get labelNewPost => 'New Post'; 304 + 305 + @override 306 + String get labelNotifications => 'Notifications'; 307 + 308 + @override 309 + String get labelOpenMenu => 'Open menu'; 310 + 311 + @override 312 + String get labelPrivacyPolicy => 'Privacy Policy'; 313 + 314 + @override 315 + String get labelProfile => 'PROFILE'; 316 + 317 + @override 318 + String get labelProviderDiagnostics => 'Provider Diagnostics'; 319 + 320 + @override 321 + String get labelRefreshProviderHealth => 'Refresh Provider Health'; 322 + 323 + @override 324 + String get labelResetSignInData => 'Reset Sign-In Data'; 325 + 326 + @override 327 + String get labelRoamTheAtmosphere => 'Roam the ATmosphere'; 328 + 329 + @override 330 + String get labelSearch => 'Search'; 331 + 332 + @override 333 + String get labelSearchPosts => 'Search Posts'; 334 + 335 + @override 336 + String get labelJumpToProfile => 'Jump to profile'; 337 + 338 + @override 339 + String get labelPosts => 'Posts'; 340 + 341 + @override 342 + String get labelPeople => 'People'; 343 + 344 + @override 345 + String get labelStarterPacks => 'Starter Packs'; 346 + 347 + @override 348 + String get labelSortBy => 'Sort by'; 349 + 350 + @override 351 + String get labelTop => 'Top'; 352 + 353 + @override 354 + String get labelLatest => 'Latest'; 355 + 356 + @override 357 + String get labelFilters => 'Filters'; 358 + 359 + @override 360 + String get labelPostFilters => 'Post filters'; 361 + 362 + @override 363 + String get labelMentions => 'Mentions'; 364 + 365 + @override 366 + String get labelAuthor => 'Author'; 367 + 368 + @override 369 + String get labelAuthorFixed => 'Author (fixed)'; 370 + 371 + @override 372 + String get labelLanguage => 'Language'; 373 + 374 + @override 375 + String get labelDomain => 'Domain'; 376 + 377 + @override 378 + String get labelUrl => 'URL'; 379 + 380 + @override 381 + String get labelTags => 'Tags'; 382 + 383 + @override 384 + String get labelSince => 'Since'; 385 + 386 + @override 387 + String get labelUntil => 'Until'; 388 + 389 + @override 390 + String get labelClearSince => 'Clear since'; 391 + 392 + @override 393 + String get labelClearUntil => 'Clear until'; 394 + 395 + @override 396 + String get labelSavedAccounts => 'Saved accounts'; 397 + 398 + @override 399 + String get labelSearchNav => 'SEARCH'; 400 + 401 + @override 402 + String get labelSemanticSearch => 'Semantic Search'; 403 + 404 + @override 405 + String get labelSettings => 'Settings'; 406 + 407 + @override 408 + String get labelShow => 'Show'; 409 + 410 + @override 411 + String get labelSignInRequired => 'Sign in required'; 412 + 413 + @override 414 + String get labelSlingshotIdentityFallback => 'Slingshot Identity Fallback'; 415 + 416 + @override 417 + String get labelStartingSignIn => 'Starting sign in'; 418 + 419 + @override 420 + String get labelSystem => 'System'; 421 + 422 + @override 423 + String get labelTermsOfService => 'Terms of Service'; 424 + 425 + @override 426 + String get labelTheme => 'THEME'; 427 + 428 + @override 429 + String get labelThreadAutoCollapse => 'Thread Auto-Collapse'; 430 + 431 + @override 432 + String get labelTroubleshooting => 'Troubleshooting'; 433 + 434 + @override 435 + String get labelTypeaheadProvider => 'Typeahead Provider'; 436 + 437 + @override 438 + String get labelVideoUploadLimits => 'Video Upload Limits'; 439 + 440 + @override 441 + String get messageAppPasswordGeneratedViaBluesky => 442 + 'Can be generated via Bluesky\'s App Passwords section at bsky.app.'; 443 + 444 + @override 445 + String get messageAppViewDebug401Armed => 'Armed: next XRPC request will return debug 401 Unauthorized'; 446 + 447 + @override 448 + String get messageBlueskyEndpointSelected => 'Bluesky official endpoint selected.'; 449 + 450 + @override 451 + String get messageBookmarksAndLikesSubtitle => 'View your bookmarked and liked posts'; 452 + 453 + @override 454 + String get messageChooseProviderSubtitle => 'Choose the AppView provider used for sign in and public reads.'; 455 + 456 + @override 457 + String get messageCleanFollowsSubtitle => 'Audit and unfollow problematic accounts in bulk'; 458 + 459 + @override 460 + String get messageClearCacheSubtitle => 461 + 'Remove cached posts, profiles, images, feeds, threads, and semantic search data'; 462 + 463 + @override 464 + String get messageCommunityTypeaheadSelected => 'Community (waow.tech) selected. Third-party service.'; 465 + 466 + @override 467 + String get messageContentModerationSubtitle => 'Manage labelers and visibility rules'; 468 + 469 + @override 470 + String get messageAdultContentEnabled => '18+ labels can be configured'; 471 + 472 + @override 473 + String get messageAdultContentRequired => 'Required before 18+ labels can be configured'; 474 + 475 + @override 476 + String get messageCrashReportingDisabled => 'Disabled. Crash and error reports are not sent.'; 477 + 478 + @override 479 + String get messageCrashReportingEnabled => 'Enabled. Crash and error reports are sent to improve stability.'; 480 + 481 + @override 482 + String get messageCrashlyticsTestCrashSubtitle => 'Intentionally crash to validate Crashlytics reports'; 483 + 484 + @override 485 + String get messageCrossProviderFallbackSubtitle => 486 + 'Retry public reads on the alternate AppView when transient errors occur'; 487 + 488 + @override 489 + String get messageDeveloperGoOfflineSubtitle => 'Turn off online connectivity'; 490 + 491 + @override 492 + String get messageFeedLayoutCard => 'Card'; 493 + 494 + @override 495 + String get messageLoadingSavedAccounts => 'Loading saved accounts...'; 496 + 497 + @override 498 + String get messageFeedLayoutCompact => 'Compact'; 499 + 500 + @override 501 + String get messageFeedsSubtitle => 'Manage pinned and saved feeds'; 502 + 503 + @override 504 + String get messageForceNextXrpc401Subtitle => 505 + 'Debug-only: next network request returns Unauthorized to test token refresh'; 506 + 507 + @override 508 + String get messageManageSemanticSearchSubtitle => 'Manage semantic search from Bookmarks & Likes -> Search'; 509 + 510 + @override 511 + String get messageModeratedContentCannotReveal => 'Hidden by your moderation settings and cannot be revealed here.'; 512 + 513 + @override 514 + String get messageModeratedContentCanReveal => 'Hidden by your moderation settings. You can reveal it for this view.'; 515 + 516 + @override 517 + String get messageProviderDiagnosticsSubtitle => 518 + 'Moderation/ranking can differ by provider. Verify health and recent fallback state.'; 519 + 520 + @override 521 + String get messageRefreshProviderHealthSubtitle => 'Probe public AppView endpoints now'; 522 + 523 + @override 524 + String get messageResetSignInDataSubtitle => 525 + 'Troubleshoot OAuth or account-switching issues by clearing local sessions on this device'; 526 + 527 + @override 528 + String get messageSearchSubtitle => 'Search'; 529 + 530 + @override 531 + String get messageClearSearchHistoryContent => 'This will delete all your recent searches.'; 532 + 533 + @override 534 + String get messageClearSearchHistoryTitle => 'Clear search history?'; 535 + 536 + @override 537 + String get messageStartTypingToSearchHandles => 'Start typing to search handles.'; 538 + 539 + @override 540 + String get messageStarterPackSearchApiUnavailable => 'Starter pack search is not available in the API yet.'; 541 + 542 + @override 543 + String get messageStarterPackSearchUnavailableTitle => 'Starter Pack Search Is Unavailable'; 544 + 545 + @override 546 + String get messageStarterPackSearchUnavailableBody => 547 + '(Starter Pack Search is not yet implemented in the BlueSky API)'; 548 + 549 + @override 550 + String get messageTrackApiProgress => 'Track API progress'; 551 + 552 + @override 553 + String get messageCouldNotOpenIssueLink => 'Could not open issue link.'; 554 + 555 + @override 556 + String get messageSearchPostsPlaceholder => 'Search posts'; 557 + 558 + @override 559 + String get messageSearchThisProfilesPostsPlaceholder => 'Search this profile\'s posts'; 560 + 561 + @override 562 + String get messageSearchPeoplePlaceholder => 'Search people'; 563 + 564 + @override 565 + String get messageSearchFeedsPlaceholder => 'Search feeds'; 566 + 567 + @override 568 + String get messageStarterPackSearchUnavailablePlaceholder => 'Starter pack search unavailable'; 569 + 570 + @override 571 + String get messageSlingshotIdentityFallbackSubtitle => 572 + 'If handle lookup fails, use Slingshot to find your DID and PDS so sign-in can continue'; 573 + 574 + @override 575 + String get messageThreadAutoCollapseSubtitle => 'Collapse reply branches deeper than the selected level'; 576 + 577 + @override 578 + String get messageTurnOffNonEssentialMotion => 'Turn off non-essential motion effects'; 579 + 580 + @override 581 + String get messageVideoUploadLimitsSubtitle => 'Check your daily video quota'; 582 + 583 + @override 584 + String get placeholderAppPassword => 'xxxx-xxxx-xxxx-xxxx'; 585 + 586 + @override 587 + String get placeholderHandleOrDid => 'username.bsky.social or did:plc:...'; 588 + 589 + @override 590 + String get promptHandleOrDid => 'Handle or DID'; 591 + 592 + @override 593 + String get validationEnterAppPassword => 'Enter your app password'; 594 + }
+776
lib/core/l10n/intl_en.arb
··· 1 + { 2 + "@@locale": "en", 3 + "@@context": "Lazurite English localization strings", 4 + "appTitle": "Lazurite", 5 + "@appTitle": { 6 + "description": "Application title" 7 + }, 8 + "buttonApplyAndRestart": "Apply and Restart", 9 + "@buttonApplyAndRestart": { 10 + "description": "Button label to apply a setting and restart app services" 11 + }, 12 + "buttonCancel": "Cancel", 13 + "@buttonCancel": { 14 + "description": "Cancel button label" 15 + }, 16 + "buttonClearCache": "Clear Cache", 17 + "@buttonClearCache": { 18 + "description": "Button label to clear local cache" 19 + }, 20 + "buttonContinue": "Continue", 21 + "@buttonContinue": { 22 + "description": "Continue button label" 23 + }, 24 + "buttonRemove": "Remove", 25 + "@buttonRemove": { 26 + "description": "Remove button label" 27 + }, 28 + "buttonApply": "Apply", 29 + "@buttonApply": { 30 + "description": "Apply button label" 31 + }, 32 + "buttonClear": "Clear", 33 + "@buttonClear": { 34 + "description": "Clear button label" 35 + }, 36 + "buttonClearAll": "Clear all", 37 + "@buttonClearAll": { 38 + "description": "Clear all filters button label" 39 + }, 40 + "buttonOpen": "Open", 41 + "@buttonOpen": { 42 + "description": "Open button label" 43 + }, 44 + "buttonResetSignInData": "Reset Sign-In Data", 45 + "@buttonResetSignInData": { 46 + "description": "Button label to clear local sign-in data" 47 + }, 48 + "buttonRetry": "Retry", 49 + "@buttonRetry": { 50 + "description": "Retry button label" 51 + }, 52 + "buttonSignIn": "Sign In", 53 + "@buttonSignIn": { 54 + "description": "Sign in button label" 55 + }, 56 + "buttonShowContent": "Show content", 57 + "@buttonShowContent": { 58 + "description": "Button label to reveal moderated content" 59 + }, 60 + "buttonTryAgain": "Try again", 61 + "@buttonTryAgain": { 62 + "description": "Try again button label" 63 + }, 64 + "commonNever": "Never", 65 + "@commonNever": { 66 + "description": "Label for a value that has never happened" 67 + }, 68 + "commonNone": "None", 69 + "@commonNone": { 70 + "description": "Label for no value" 71 + }, 72 + "commonNotCheckedYet": "Not checked yet", 73 + "@commonNotCheckedYet": { 74 + "description": "Label for a health check that has not run" 75 + }, 76 + "commonOff": "Off", 77 + "@commonOff": { 78 + "description": "Disabled option label" 79 + }, 80 + "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 + "@dialogClearCacheContent": { 82 + "description": "Confirmation dialog body before clearing local cache" 83 + }, 84 + "dialogClearCacheTitle": "Clear cache?", 85 + "@dialogClearCacheTitle": { 86 + "description": "Confirmation dialog title before clearing local cache" 87 + }, 88 + "dialogRemoveAccountContent": "Remove @{handle} from this device?", 89 + "@dialogRemoveAccountContent": { 90 + "description": "Confirmation dialog body before removing a saved account from this device", 91 + "placeholders": { 92 + "handle": { 93 + "type": "String", 94 + "example": "alice.bsky.social" 95 + } 96 + } 97 + }, 98 + "dialogRemoveAccountTitle": "Remove Account", 99 + "@dialogRemoveAccountTitle": { 100 + "description": "Confirmation dialog title before removing a saved account" 101 + }, 102 + "dialogResetSignInDataContent": "Use this only when troubleshooting sign-in or account switching.\n\nThis clears all local account sessions on this device and sends you back to sign in. It does not delete your Bluesky account or posts.", 103 + "@dialogResetSignInDataContent": { 104 + "description": "Confirmation dialog body before clearing local sign-in data" 105 + }, 106 + "dialogResetSignInDataTitle": "Reset sign-in data?", 107 + "@dialogResetSignInDataTitle": { 108 + "description": "Confirmation dialog title before clearing local sign-in data" 109 + }, 110 + "dialogSwitchAppViewProviderContent": "Apply and restart now to rebuild network services.\n\nYou will stay signed in and no local data will be deleted.\n\nModeration labels, ranking, and trending results can differ between providers.", 111 + "@dialogSwitchAppViewProviderContent": { 112 + "description": "Confirmation dialog body before switching AppView provider" 113 + }, 114 + "dialogSwitchAppViewProviderTitle": "Switch AppView provider?", 115 + "@dialogSwitchAppViewProviderTitle": { 116 + "description": "Confirmation dialog title before switching AppView provider" 117 + }, 118 + "errorFailedToSaveProviderSelection": "Failed to save provider selection: {error}", 119 + "@errorFailedToSaveProviderSelection": { 120 + "description": "Snackbar message when persisting selected provider fails", 121 + "placeholders": { 122 + "error": { 123 + "type": "Object", 124 + "example": "database unavailable" 125 + } 126 + } 127 + }, 128 + "errorFailedToClearCache": "Failed to clear cache: {error}", 129 + "@errorFailedToClearCache": { 130 + "description": "Snackbar message when cache clearing fails", 131 + "placeholders": { 132 + "error": { 133 + "type": "Object", 134 + "example": "permission denied" 135 + } 136 + } 137 + }, 138 + "errorGenericTitle": "Something went wrong", 139 + "@errorGenericTitle": { 140 + "description": "Generic error state title" 141 + }, 142 + "errorUnableToRemoveAccount": "Unable to remove account right now.", 143 + "@errorUnableToRemoveAccount": { 144 + "description": "Snackbar message when a saved account cannot be removed" 145 + }, 146 + "labelRemoveAccount": "Remove account", 147 + "@labelRemoveAccount": { 148 + "description": "Tooltip for removing a saved account" 149 + }, 150 + "formatAccountsTapToSwitch": "{count, plural, =1{1 account - tap to switch} other{{count} accounts - tap to switch}}", 151 + "@formatAccountsTapToSwitch": { 152 + "description": "Settings account row subtitle when multiple accounts are available", 153 + "placeholders": { 154 + "count": { 155 + "type": "int", 156 + "example": "2" 157 + } 158 + } 159 + }, 160 + "formatAppViewProviderSelected": "{provider} selected. Switching providers performs a soft restart.", 161 + "@formatAppViewProviderSelected": { 162 + "description": "Settings subtitle for selected AppView provider", 163 + "placeholders": { 164 + "provider": { 165 + "type": "String", 166 + "example": "Bluesky" 167 + } 168 + } 169 + }, 170 + "formatContentModerationCustomLabelers": "{count, plural, =0{No custom labelers subscribed} =1{1 custom labeler subscribed} other{{count} custom labelers subscribed}}", 171 + "@formatContentModerationCustomLabelers": { 172 + "description": "Settings subtitle showing the count of subscribed custom moderation labelers", 173 + "placeholders": { 174 + "count": { 175 + "type": "int", 176 + "example": "2" 177 + } 178 + } 179 + }, 180 + "formatDepth": "Depth {depth}", 181 + "@formatDepth": { 182 + "description": "Thread auto-collapse depth setting option", 183 + "placeholders": { 184 + "depth": { 185 + "type": "int", 186 + "example": "3" 187 + } 188 + } 189 + }, 190 + "labelAbout": "About", 191 + "@labelAbout": { 192 + "description": "About page or settings label" 193 + }, 194 + "labelAccount": "Account", 195 + "@labelAccount": { 196 + "description": "Account settings section label" 197 + }, 198 + "labelAccountMaintenance": "Account Maintenance", 199 + "@labelAccountMaintenance": { 200 + "description": "Settings section for account maintenance actions" 201 + }, 202 + "labelActiveProvider": "Active Provider", 203 + "@labelActiveProvider": { 204 + "description": "Provider diagnostics label for active provider" 205 + }, 206 + "labelAdultContent": "Adult Content", 207 + "@labelAdultContent": { 208 + "description": "Adult content setting label" 209 + }, 210 + "labelAdvanced": "Advanced", 211 + "@labelAdvanced": { 212 + "description": "Advanced settings section label" 213 + }, 214 + "labelAlerts": "ALERTS", 215 + "@labelAlerts": { 216 + "description": "Bottom navigation alerts label" 217 + }, 218 + "labelAnimations": "Animations", 219 + "@labelAnimations": { 220 + "description": "Animations settings label" 221 + }, 222 + "labelAppearance": "Appearance", 223 + "@labelAppearance": { 224 + "description": "Appearance settings section label" 225 + }, 226 + "labelAppPassword": "App Password", 227 + "@labelAppPassword": { 228 + "description": "Debug app password field label" 229 + }, 230 + "labelAppPasswordLogin": "App Password Login", 231 + "@labelAppPasswordLogin": { 232 + "description": "Debug app password login card title" 233 + }, 234 + "labelAppViewProvider": "AppView Provider", 235 + "@labelAppViewProvider": { 236 + "description": "AppView provider settings label" 237 + }, 238 + "labelAtExplorer": "AT Explorer", 239 + "@labelAtExplorer": { 240 + "description": "AT Protocol explorer settings/menu label" 241 + }, 242 + "labelAtProtocolConnection": "AT Protocol Connection", 243 + "@labelAtProtocolConnection": { 244 + "description": "Settings section title for AT Protocol connection details" 245 + }, 246 + "labelAuditFollows": "Audit Follows", 247 + "@labelAuditFollows": { 248 + "description": "Menu item for follow audit feature" 249 + }, 250 + "labelBack": "Back", 251 + "@labelBack": { 252 + "description": "Back tooltip label" 253 + }, 254 + "labelBookmarksAndLikes": "Bookmarks & Likes", 255 + "@labelBookmarksAndLikes": { 256 + "description": "Settings item for bookmarked and liked posts" 257 + }, 258 + "labelCacheCleared": "Cache cleared", 259 + "@labelCacheCleared": { 260 + "description": "Snackbar message after clearing cache" 261 + }, 262 + "labelChooseYourPortal": "Choose your portal", 263 + "@labelChooseYourPortal": { 264 + "description": "Login provider selection label" 265 + }, 266 + "labelCleanFollows": "Clean Follows", 267 + "@labelCleanFollows": { 268 + "description": "Settings item for follow audit feature" 269 + }, 270 + "labelClearCache": "Clear Cache", 271 + "@labelClearCache": { 272 + "description": "Settings item for clearing cache" 273 + }, 274 + "labelCommunity": "Community", 275 + "@labelCommunity": { 276 + "description": "Community provider option label" 277 + }, 278 + "labelConstellationUrl": "Constellation URL", 279 + "@labelConstellationUrl": { 280 + "description": "Constellation URL setting label" 281 + }, 282 + "labelContentModeration": "Content Moderation", 283 + "@labelContentModeration": { 284 + "description": "Content moderation settings label" 285 + }, 286 + "labelContinueSignIn": "Continue sign in", 287 + "@labelContinueSignIn": { 288 + "description": "Semantic label for continuing sign in" 289 + }, 290 + "labelCrashReporting": "Crash Reporting", 291 + "@labelCrashReporting": { 292 + "description": "Crash reporting setting label" 293 + }, 294 + "labelCrashlyticsTestCrash": "Crashlytics Test Crash", 295 + "@labelCrashlyticsTestCrash": { 296 + "description": "Developer setting to trigger a test crash" 297 + }, 298 + "labelCrossProviderFallback": "Cross-Provider Fallback", 299 + "@labelCrossProviderFallback": { 300 + "description": "Cross-provider fallback setting label" 301 + }, 302 + "labelDangerZone": "Danger Zone", 303 + "@labelDangerZone": { 304 + "description": "Destructive settings section label" 305 + }, 306 + "labelDark": "Dark", 307 + "@labelDark": { 308 + "description": "Dark theme option label" 309 + }, 310 + "labelDebug": "Debug", 311 + "@labelDebug": { 312 + "description": "Debug section label" 313 + }, 314 + "labelDeveloper": "Developer", 315 + "@labelDeveloper": { 316 + "description": "Developer settings section label" 317 + }, 318 + "labelFeedLayout": "Feed Layout", 319 + "@labelFeedLayout": { 320 + "description": "Feed layout setting label" 321 + }, 322 + "labelFeeds": "Feeds", 323 + "@labelFeeds": { 324 + "description": "Feeds menu/settings label" 325 + }, 326 + "labelForceNextXrpc401": "Force Next XRPC 401", 327 + "@labelForceNextXrpc401": { 328 + "description": "Developer setting to force the next XRPC request to return Unauthorized" 329 + }, 330 + "labelGoOffline": "Go Offline", 331 + "@labelGoOffline": { 332 + "description": "Developer setting to simulate offline mode" 333 + }, 334 + "labelGuest": "Guest", 335 + "@labelGuest": { 336 + "description": "Fallback account display name when no user is signed in" 337 + }, 338 + "labelHealth": "Health", 339 + "@labelHealth": { 340 + "description": "Provider diagnostics health label" 341 + }, 342 + "labelHandle": "Handle", 343 + "@labelHandle": { 344 + "description": "Handle field label" 345 + }, 346 + "labelHide": "Hide", 347 + "@labelHide": { 348 + "description": "Hide button label" 349 + }, 350 + "labelHome": "HOME", 351 + "@labelHome": { 352 + "description": "Bottom navigation home label" 353 + }, 354 + "labelLastError": "Last Error", 355 + "@labelLastError": { 356 + "description": "Provider diagnostics last error label" 357 + }, 358 + "labelLastFallback": "Last Fallback", 359 + "@labelLastFallback": { 360 + "description": "Provider diagnostics last fallback label" 361 + }, 362 + "labelLastHealthCheck": "Last Health Check", 363 + "@labelLastHealthCheck": { 364 + "description": "Provider diagnostics last health check label" 365 + }, 366 + "labelLayout": "Layout", 367 + "@labelLayout": { 368 + "description": "Layout settings section label" 369 + }, 370 + "labelLight": "Light", 371 + "@labelLight": { 372 + "description": "Light theme option label" 373 + }, 374 + "labelLogOut": "Log Out", 375 + "@labelLogOut": { 376 + "description": "Log out button or menu label" 377 + }, 378 + "labelLogs": "Logs", 379 + "@labelLogs": { 380 + "description": "Logs settings label" 381 + }, 382 + "labelMessages": "Messages", 383 + "@labelMessages": { 384 + "description": "Messages menu label" 385 + }, 386 + "labelModeration": "Moderation", 387 + "@labelModeration": { 388 + "description": "Moderation settings section label" 389 + }, 390 + "labelNavigation": "Navigation", 391 + "@labelNavigation": { 392 + "description": "Navigation menu section label" 393 + }, 394 + "labelNewPost": "New Post", 395 + "@labelNewPost": { 396 + "description": "New post menu label" 397 + }, 398 + "labelNotifications": "Notifications", 399 + "@labelNotifications": { 400 + "description": "Notifications menu label" 401 + }, 402 + "labelOpenMenu": "Open menu", 403 + "@labelOpenMenu": { 404 + "description": "Tooltip for opening the navigation menu" 405 + }, 406 + "labelPrivacyPolicy": "Privacy Policy", 407 + "@labelPrivacyPolicy": { 408 + "description": "Privacy policy link label" 409 + }, 410 + "labelProfile": "PROFILE", 411 + "@labelProfile": { 412 + "description": "Bottom navigation profile label" 413 + }, 414 + "labelProviderDiagnostics": "Provider Diagnostics", 415 + "@labelProviderDiagnostics": { 416 + "description": "Settings section for provider diagnostics" 417 + }, 418 + "labelRefreshProviderHealth": "Refresh Provider Health", 419 + "@labelRefreshProviderHealth": { 420 + "description": "Settings item for refreshing provider health" 421 + }, 422 + "labelResetSignInData": "Reset Sign-In Data", 423 + "@labelResetSignInData": { 424 + "description": "Settings item for resetting local sign-in data" 425 + }, 426 + "labelRoamTheAtmosphere": "Roam the ATmosphere", 427 + "@labelRoamTheAtmosphere": { 428 + "description": "Login screen subtitle" 429 + }, 430 + "labelSearch": "Search", 431 + "@labelSearch": { 432 + "description": "Search menu/settings label" 433 + }, 434 + "labelSearchPosts": "Search Posts", 435 + "@labelSearchPosts": { 436 + "description": "Title for posts-only search mode" 437 + }, 438 + "labelJumpToProfile": "Jump to profile", 439 + "@labelJumpToProfile": { 440 + "description": "Button and dialog title for jumping directly to a profile" 441 + }, 442 + "labelPosts": "Posts", 443 + "@labelPosts": { 444 + "description": "Posts tab label" 445 + }, 446 + "labelPeople": "People", 447 + "@labelPeople": { 448 + "description": "People tab label" 449 + }, 450 + "labelStarterPacks": "Starter Packs", 451 + "@labelStarterPacks": { 452 + "description": "Starter packs tab label" 453 + }, 454 + "labelSortBy": "Sort by", 455 + "@labelSortBy": { 456 + "description": "Sort control label" 457 + }, 458 + "labelTop": "Top", 459 + "@labelTop": { 460 + "description": "Top sort option label" 461 + }, 462 + "labelLatest": "Latest", 463 + "@labelLatest": { 464 + "description": "Latest sort option label" 465 + }, 466 + "labelFilters": "Filters", 467 + "@labelFilters": { 468 + "description": "Search filters button label" 469 + }, 470 + "labelPostFilters": "Post filters", 471 + "@labelPostFilters": { 472 + "description": "Post filters sheet title" 473 + }, 474 + "labelMentions": "Mentions", 475 + "@labelMentions": { 476 + "description": "Mentions filter label" 477 + }, 478 + "labelAuthor": "Author", 479 + "@labelAuthor": { 480 + "description": "Author filter label" 481 + }, 482 + "labelAuthorFixed": "Author (fixed)", 483 + "@labelAuthorFixed": { 484 + "description": "Fixed author filter label" 485 + }, 486 + "labelLanguage": "Language", 487 + "@labelLanguage": { 488 + "description": "Language filter label" 489 + }, 490 + "labelDomain": "Domain", 491 + "@labelDomain": { 492 + "description": "Domain filter label" 493 + }, 494 + "labelUrl": "URL", 495 + "@labelUrl": { 496 + "description": "URL filter label" 497 + }, 498 + "labelTags": "Tags", 499 + "@labelTags": { 500 + "description": "Tags filter label" 501 + }, 502 + "labelSince": "Since", 503 + "@labelSince": { 504 + "description": "Since date filter label" 505 + }, 506 + "labelUntil": "Until", 507 + "@labelUntil": { 508 + "description": "Until date filter label" 509 + }, 510 + "labelClearSince": "Clear since", 511 + "@labelClearSince": { 512 + "description": "Button label to clear since date filter" 513 + }, 514 + "labelClearUntil": "Clear until", 515 + "@labelClearUntil": { 516 + "description": "Button label to clear until date filter" 517 + }, 518 + "labelSavedAccounts": "Saved accounts", 519 + "@labelSavedAccounts": { 520 + "description": "Saved accounts section label on login screen" 521 + }, 522 + "labelSearchNav": "SEARCH", 523 + "@labelSearchNav": { 524 + "description": "Bottom navigation search label" 525 + }, 526 + "labelSemanticSearch": "Semantic Search", 527 + "@labelSemanticSearch": { 528 + "description": "Semantic search settings label" 529 + }, 530 + "labelSettings": "Settings", 531 + "@labelSettings": { 532 + "description": "Settings page title or tooltip" 533 + }, 534 + "labelShow": "Show", 535 + "@labelShow": { 536 + "description": "Show button label" 537 + }, 538 + "labelSignInRequired": "Sign in required", 539 + "@labelSignInRequired": { 540 + "description": "Fallback account handle text when sign in is required" 541 + }, 542 + "labelSlingshotIdentityFallback": "Slingshot Identity Fallback", 543 + "@labelSlingshotIdentityFallback": { 544 + "description": "Slingshot identity fallback setting label" 545 + }, 546 + "labelStartingSignIn": "Starting sign in", 547 + "@labelStartingSignIn": { 548 + "description": "Tooltip while sign in is starting" 549 + }, 550 + "labelSystem": "System", 551 + "@labelSystem": { 552 + "description": "System theme option label" 553 + }, 554 + "labelTermsOfService": "Terms of Service", 555 + "@labelTermsOfService": { 556 + "description": "Terms of service link label" 557 + }, 558 + "labelTheme": "THEME", 559 + "@labelTheme": { 560 + "description": "Theme subsection label in settings" 561 + }, 562 + "labelThreadAutoCollapse": "Thread Auto-Collapse", 563 + "@labelThreadAutoCollapse": { 564 + "description": "Thread auto-collapse setting label" 565 + }, 566 + "labelTroubleshooting": "Troubleshooting", 567 + "@labelTroubleshooting": { 568 + "description": "Troubleshooting settings section label" 569 + }, 570 + "labelTypeaheadProvider": "Typeahead Provider", 571 + "@labelTypeaheadProvider": { 572 + "description": "Typeahead provider settings label" 573 + }, 574 + "labelVideoUploadLimits": "Video Upload Limits", 575 + "@labelVideoUploadLimits": { 576 + "description": "Video upload limits settings label" 577 + }, 578 + "messageAppPasswordGeneratedViaBluesky": "Can be generated via Bluesky''s App Passwords section at bsky.app.", 579 + "@messageAppPasswordGeneratedViaBluesky": { 580 + "description": "Debug app password help text" 581 + }, 582 + "messageAppViewDebug401Armed": "Armed: next XRPC request will return debug 401 Unauthorized", 583 + "@messageAppViewDebug401Armed": { 584 + "description": "Snackbar message after arming debug unauthorized response" 585 + }, 586 + "messageBlueskyEndpointSelected": "Bluesky official endpoint selected.", 587 + "@messageBlueskyEndpointSelected": { 588 + "description": "Settings subtitle for Bluesky typeahead provider" 589 + }, 590 + "messageBookmarksAndLikesSubtitle": "View your bookmarked and liked posts", 591 + "@messageBookmarksAndLikesSubtitle": { 592 + "description": "Settings subtitle for bookmarks and likes" 593 + }, 594 + "messageChooseProviderSubtitle": "Choose the AppView provider used for sign in and public reads.", 595 + "@messageChooseProviderSubtitle": { 596 + "description": "Login screen provider selection helper text" 597 + }, 598 + "messageCleanFollowsSubtitle": "Audit and unfollow problematic accounts in bulk", 599 + "@messageCleanFollowsSubtitle": { 600 + "description": "Settings subtitle for clean follows" 601 + }, 602 + "messageClearCacheSubtitle": "Remove cached posts, profiles, images, feeds, threads, and semantic search data", 603 + "@messageClearCacheSubtitle": { 604 + "description": "Settings subtitle for clearing cache" 605 + }, 606 + "messageCommunityTypeaheadSelected": "Community (waow.tech) selected. Third-party service.", 607 + "@messageCommunityTypeaheadSelected": { 608 + "description": "Settings subtitle for community typeahead provider" 609 + }, 610 + "messageContentModerationSubtitle": "Manage labelers and visibility rules", 611 + "@messageContentModerationSubtitle": { 612 + "description": "Settings subtitle for content moderation" 613 + }, 614 + "messageAdultContentEnabled": "18+ labels can be configured", 615 + "@messageAdultContentEnabled": { 616 + "description": "Settings subtitle when adult content preference is enabled" 617 + }, 618 + "messageAdultContentRequired": "Required before 18+ labels can be configured", 619 + "@messageAdultContentRequired": { 620 + "description": "Settings subtitle when adult content preference is disabled" 621 + }, 622 + "messageCrashReportingDisabled": "Disabled. Crash and error reports are not sent.", 623 + "@messageCrashReportingDisabled": { 624 + "description": "Settings subtitle when crash reporting is disabled" 625 + }, 626 + "messageCrashReportingEnabled": "Enabled. Crash and error reports are sent to improve stability.", 627 + "@messageCrashReportingEnabled": { 628 + "description": "Settings subtitle when crash reporting is enabled" 629 + }, 630 + "messageCrashlyticsTestCrashSubtitle": "Intentionally crash to validate Crashlytics reports", 631 + "@messageCrashlyticsTestCrashSubtitle": { 632 + "description": "Developer setting subtitle for test crash" 633 + }, 634 + "messageCrossProviderFallbackSubtitle": "Retry public reads on the alternate AppView when transient errors occur", 635 + "@messageCrossProviderFallbackSubtitle": { 636 + "description": "Settings subtitle for cross-provider fallback" 637 + }, 638 + "messageDeveloperGoOfflineSubtitle": "Turn off online connectivity", 639 + "@messageDeveloperGoOfflineSubtitle": { 640 + "description": "Developer setting subtitle for simulated offline" 641 + }, 642 + "messageFeedLayoutCard": "Card", 643 + "@messageFeedLayoutCard": { 644 + "description": "Feed layout card option" 645 + }, 646 + "messageLoadingSavedAccounts": "Loading saved accounts...", 647 + "@messageLoadingSavedAccounts": { 648 + "description": "Loading message while saved accounts load" 649 + }, 650 + "messageFeedLayoutCompact": "Compact", 651 + "@messageFeedLayoutCompact": { 652 + "description": "Feed layout compact option" 653 + }, 654 + "messageFeedsSubtitle": "Manage pinned and saved feeds", 655 + "@messageFeedsSubtitle": { 656 + "description": "Settings subtitle for feeds" 657 + }, 658 + "messageForceNextXrpc401Subtitle": "Debug-only: next network request returns Unauthorized to test token refresh", 659 + "@messageForceNextXrpc401Subtitle": { 660 + "description": "Developer setting subtitle for forced 401" 661 + }, 662 + "messageManageSemanticSearchSubtitle": "Manage semantic search from Bookmarks & Likes -> Search", 663 + "@messageManageSemanticSearchSubtitle": { 664 + "description": "Settings subtitle for semantic search management" 665 + }, 666 + 667 + "messageModeratedContentCannotReveal": "Hidden by your moderation settings and cannot be revealed here.", 668 + "@messageModeratedContentCannotReveal": { 669 + "description": "Moderation overlay description when content cannot be revealed" 670 + }, 671 + 672 + "messageModeratedContentCanReveal": "Hidden by your moderation settings. You can reveal it for this view.", 673 + "@messageModeratedContentCanReveal": { 674 + "description": "Moderation overlay description when content can be revealed" 675 + }, 676 + "messageProviderDiagnosticsSubtitle": "Moderation/ranking can differ by provider. Verify health and recent fallback state.", 677 + "@messageProviderDiagnosticsSubtitle": { 678 + "description": "Settings subtitle for provider diagnostics" 679 + }, 680 + "messageRefreshProviderHealthSubtitle": "Probe public AppView endpoints now", 681 + "@messageRefreshProviderHealthSubtitle": { 682 + "description": "Settings subtitle for refreshing provider health" 683 + }, 684 + "messageResetSignInDataSubtitle": "Troubleshoot OAuth or account-switching issues by clearing local sessions on this device", 685 + "@messageResetSignInDataSubtitle": { 686 + "description": "Settings subtitle for resetting sign-in data" 687 + }, 688 + "messageSearchSubtitle": "Search", 689 + "@messageSearchSubtitle": { 690 + "description": "Generic search subtitle" 691 + }, 692 + "messageClearSearchHistoryContent": "This will delete all your recent searches.", 693 + "@messageClearSearchHistoryContent": { 694 + "description": "Confirmation dialog body before clearing search history" 695 + }, 696 + "messageClearSearchHistoryTitle": "Clear search history?", 697 + "@messageClearSearchHistoryTitle": { 698 + "description": "Confirmation dialog title before clearing search history" 699 + }, 700 + "messageStartTypingToSearchHandles": "Start typing to search handles.", 701 + "@messageStartTypingToSearchHandles": { 702 + "description": "Hint shown in jump to profile dialog" 703 + }, 704 + "messageStarterPackSearchApiUnavailable": "Starter pack search is not available in the API yet.", 705 + "@messageStarterPackSearchApiUnavailable": { 706 + "description": "Helper text when starter pack search is disabled" 707 + }, 708 + "messageStarterPackSearchUnavailableTitle": "Starter Pack Search Is Unavailable", 709 + "@messageStarterPackSearchUnavailableTitle": { 710 + "description": "Starter pack search unavailable state title" 711 + }, 712 + "messageStarterPackSearchUnavailableBody": "(Starter Pack Search is not yet implemented in the BlueSky API)", 713 + "@messageStarterPackSearchUnavailableBody": { 714 + "description": "Starter pack search unavailable state body" 715 + }, 716 + "messageTrackApiProgress": "Track API progress", 717 + "@messageTrackApiProgress": { 718 + "description": "Button label to open upstream API issue" 719 + }, 720 + "messageCouldNotOpenIssueLink": "Could not open issue link.", 721 + "@messageCouldNotOpenIssueLink": { 722 + "description": "Snackbar message when opening upstream issue link fails" 723 + }, 724 + "messageSearchPostsPlaceholder": "Search posts", 725 + "@messageSearchPostsPlaceholder": { 726 + "description": "Search posts field placeholder" 727 + }, 728 + "messageSearchThisProfilesPostsPlaceholder": "Search this profile''s posts", 729 + "@messageSearchThisProfilesPostsPlaceholder": { 730 + "description": "Search placeholder in profile posts-only mode" 731 + }, 732 + "messageSearchPeoplePlaceholder": "Search people", 733 + "@messageSearchPeoplePlaceholder": { 734 + "description": "Search people field placeholder" 735 + }, 736 + "messageSearchFeedsPlaceholder": "Search feeds", 737 + "@messageSearchFeedsPlaceholder": { 738 + "description": "Search feeds field placeholder" 739 + }, 740 + "messageStarterPackSearchUnavailablePlaceholder": "Starter pack search unavailable", 741 + "@messageStarterPackSearchUnavailablePlaceholder": { 742 + "description": "Starter pack search field placeholder" 743 + }, 744 + "messageSlingshotIdentityFallbackSubtitle": "If handle lookup fails, use Slingshot to find your DID and PDS so sign-in can continue", 745 + "@messageSlingshotIdentityFallbackSubtitle": { 746 + "description": "Settings subtitle for Slingshot identity fallback" 747 + }, 748 + "messageThreadAutoCollapseSubtitle": "Collapse reply branches deeper than the selected level", 749 + "@messageThreadAutoCollapseSubtitle": { 750 + "description": "Settings subtitle for thread auto-collapse" 751 + }, 752 + "messageTurnOffNonEssentialMotion": "Turn off non-essential motion effects", 753 + "@messageTurnOffNonEssentialMotion": { 754 + "description": "Settings subtitle for animations" 755 + }, 756 + "messageVideoUploadLimitsSubtitle": "Check your daily video quota", 757 + "@messageVideoUploadLimitsSubtitle": { 758 + "description": "Settings subtitle for video upload limits" 759 + }, 760 + "placeholderAppPassword": "xxxx-xxxx-xxxx-xxxx", 761 + "@placeholderAppPassword": { 762 + "description": "Placeholder for app password field" 763 + }, 764 + "placeholderHandleOrDid": "username.bsky.social or did:plc:...", 765 + "@placeholderHandleOrDid": { 766 + "description": "Placeholder for handle or DID sign-in field" 767 + }, 768 + "promptHandleOrDid": "Handle or DID", 769 + "@promptHandleOrDid": { 770 + "description": "Label for handle or DID sign-in field" 771 + }, 772 + "validationEnterAppPassword": "Enter your app password", 773 + "@validationEnterAppPassword": { 774 + "description": "Validation error for missing app password" 775 + } 776 + }
+7
lib/core/l10n/l10n.dart
··· 1 + import 'package:flutter/widgets.dart'; 2 + import 'package:lazurite/core/l10n/app_localizations.dart'; 3 + 4 + extension AppLocalizationsX on BuildContext { 5 + AppLocalizations get l10n => 6 + Localizations.of<AppLocalizations>(this, AppLocalizations) ?? lookupAppLocalizations(const Locale('en')); 7 + }
+40 -36
lib/core/router/app_shell.dart
··· 7 7 import 'package:go_router/go_router.dart'; 8 8 import 'package:lazurite/core/crash_reporting/crash_reporting_consent_gate.dart'; 9 9 import 'package:lazurite/core/crash_reporting/crash_reporting_service.dart'; 10 + import 'package:lazurite/core/l10n/app_localizations.dart'; 11 + import 'package:lazurite/core/l10n/l10n.dart'; 10 12 import 'package:lazurite/core/theme/animation_tokens.dart'; 11 13 import 'package:lazurite/core/theme/animation_utils.dart'; 12 14 import 'package:lazurite/core/theme/theme_extensions.dart'; ··· 38 40 Widget build(BuildContext context) { 39 41 final shellScope = AppShellScope.maybeOf(context); 40 42 final onPressed = shellScope?.openMenu ?? AppShell.openDrawer; 41 - return IconButton(tooltip: 'Open menu', onPressed: onPressed, icon: const Icon(Icons.menu)); 43 + return IconButton(tooltip: context.l10n.labelOpenMenu, onPressed: onPressed, icon: const Icon(Icons.menu)); 42 44 } 43 45 } 44 46 ··· 94 96 @override 95 97 Widget build(BuildContext context) { 96 98 final theme = Theme.of(context); 99 + final l10n = context.l10n; 97 100 CrashReportingService? crashReportingService; 98 101 try { 99 102 crashReportingService = context.read<CrashReportingService>(); ··· 131 134 widget.navigationShell.goBranch(index, initialLocation: index == widget.navigationShell.currentIndex); 132 135 }, 133 136 labelBehavior: NavigationDestinationLabelBehavior.alwaysShow, 134 - destinations: _destinations, 137 + destinations: _destinations(l10n), 135 138 ), 136 139 ), 137 140 ), ··· 139 142 ); 140 143 } 141 144 142 - List<Widget> get _destinations => [ 143 - const NavigationDestination( 144 - icon: _AnimatedNavIcon(selected: false, outlined: Icons.home_outlined, filled: Icons.home), 145 - selectedIcon: _AnimatedNavIcon(selected: true, outlined: Icons.home_outlined, filled: Icons.home), 146 - label: 'HOME', 145 + List<Widget> _destinations(AppLocalizations l10n) => [ 146 + NavigationDestination( 147 + icon: const _AnimatedNavIcon(selected: false, outlined: Icons.home_outlined, filled: Icons.home), 148 + selectedIcon: const _AnimatedNavIcon(selected: true, outlined: Icons.home_outlined, filled: Icons.home), 149 + label: l10n.labelHome, 147 150 ), 148 - const NavigationDestination( 149 - icon: _AnimatedNavIcon(selected: false, outlined: Icons.search_outlined, filled: Icons.search), 150 - selectedIcon: _AnimatedNavIcon(selected: true, outlined: Icons.search_outlined, filled: Icons.search), 151 - label: 'SEARCH', 151 + NavigationDestination( 152 + icon: const _AnimatedNavIcon(selected: false, outlined: Icons.search_outlined, filled: Icons.search), 153 + selectedIcon: const _AnimatedNavIcon(selected: true, outlined: Icons.search_outlined, filled: Icons.search), 154 + label: l10n.labelSearchNav, 152 155 ), 153 - const NavigationDestination( 154 - icon: _AnimatedNotificationNavIcon(selected: false), 155 - selectedIcon: _AnimatedNotificationNavIcon(selected: true), 156 - label: 'ALERTS', 156 + NavigationDestination( 157 + icon: const _AnimatedNotificationNavIcon(selected: false), 158 + selectedIcon: const _AnimatedNotificationNavIcon(selected: true), 159 + label: l10n.labelAlerts, 157 160 ), 158 - const NavigationDestination( 159 - icon: _AnimatedNavIcon(selected: false, outlined: Icons.person_outline, filled: Icons.person), 160 - selectedIcon: _AnimatedNavIcon(selected: true, outlined: Icons.person_outline, filled: Icons.person), 161 - label: 'PROFILE', 161 + NavigationDestination( 162 + icon: const _AnimatedNavIcon(selected: false, outlined: Icons.person_outline, filled: Icons.person), 163 + selectedIcon: const _AnimatedNavIcon(selected: true, outlined: Icons.person_outline, filled: Icons.person), 164 + label: l10n.labelProfile, 162 165 ), 163 166 ]; 164 167 } ··· 265 268 final isMessagesRoute = currentPath.startsWith('/alerts/messages') || currentPath.startsWith('/alerts/requests'); 266 269 final isNotificationsRoute = currentPath.startsWith('/alerts') && !isMessagesRoute; 267 270 final isOffline = rootContext.read<ConnectivityCubit>().state.isOffline; 271 + final l10n = context.l10n; 268 272 final tokens = rootContext.watch<AuthBloc>().state.tokens; 269 - final displayName = tokens?.displayName ?? tokens?.handle ?? 'Guest'; 270 - final handle = tokens?.handle ?? 'Sign in required'; 273 + final displayName = tokens?.displayName ?? tokens?.handle ?? l10n.labelGuest; 274 + final handle = tokens?.handle ?? l10n.labelSignInRequired; 271 275 final did = tokens?.did; 272 276 final initials = _initialsFor(tokens?.displayName ?? tokens?.handle ?? 'L'); 273 277 final drawerWidth = (MediaQuery.sizeOf(context).width * 0.82).clamp(280.0, 320.0).toDouble(); ··· 289 293 padding: const EdgeInsets.fromLTRB(12, 8, 8, 8), 290 294 child: Row( 291 295 children: [ 292 - Text('Lazurite', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)), 296 + Text(l10n.appTitle, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)), 293 297 const Spacer(), 294 298 IconButton( 295 - tooltip: 'Close menu', 299 + tooltip: l10n.buttonCancel, 296 300 onPressed: () => Navigator.of(context).pop(), 297 301 icon: const Icon(Icons.close), 298 302 ), ··· 313 317 _MenuTile( 314 318 icon: Icons.add_circle_outline, 315 319 selectedIcon: Icons.add_circle, 316 - label: 'New Post', 320 + label: l10n.labelNewPost, 317 321 isSelected: isComposeRoute, 318 322 tooltip: isOffline ? offlineActionMessage('compose a post') : null, 319 323 onTap: isOffline ? null : () => _pushRoute(context, '/compose'), 320 324 ), 321 325 const Divider(height: 24), 322 - const _MenuSectionLabel(label: 'Navigation'), 326 + _MenuSectionLabel(label: l10n.labelNavigation), 323 327 _MenuTile( 324 328 icon: Icons.home_outlined, 325 329 selectedIcon: Icons.home, 326 - label: 'Home', 330 + label: l10n.labelHome, 327 331 isSelected: isHomeRoute, 328 332 onTap: () => _selectBranch(context, 0), 329 333 ), 330 334 _MenuTile( 331 335 icon: Icons.search_outlined, 332 336 selectedIcon: Icons.search, 333 - label: 'Search', 337 + label: l10n.labelSearch, 334 338 isSelected: isSearchRoute, 335 339 onTap: () => _selectBranch(context, 1), 336 340 ), 337 341 _MenuTile( 338 342 icon: Icons.rss_feed_outlined, 339 343 selectedIcon: Icons.rss_feed, 340 - label: 'Feeds', 344 + label: l10n.labelFeeds, 341 345 isSelected: isFeedsRoute, 342 346 onTap: () => _pushRoute(context, '/feeds'), 343 347 ), 344 348 _MenuTile( 345 349 icon: Icons.notifications_outlined, 346 350 selectedIcon: Icons.notifications, 347 - label: 'Notifications', 351 + label: l10n.labelNotifications, 348 352 isSelected: isNotificationsRoute, 349 353 trailing: _notificationsBadge(), 350 354 onTap: () => _goRoute(context, '/alerts'), ··· 352 356 _MenuTile( 353 357 icon: Icons.chat_bubble_outline, 354 358 selectedIcon: Icons.chat_bubble, 355 - label: 'Messages', 359 + label: l10n.labelMessages, 356 360 isSelected: isMessagesRoute, 357 361 onTap: () => _goRoute(context, '/alerts/messages'), 358 362 ), 359 363 _MenuTile( 360 364 icon: Icons.person_outline, 361 365 selectedIcon: Icons.person, 362 - label: 'Profile', 366 + label: l10n.labelProfile, 363 367 isSelected: isProfileRoute, 364 368 onTap: () => _selectBranch(context, 3), 365 369 ), 366 370 const Divider(height: 24), 367 - const _MenuSectionLabel(label: 'Advanced'), 371 + _MenuSectionLabel(label: l10n.labelAdvanced), 368 372 _MenuTile( 369 373 icon: Icons.explore_outlined, 370 374 selectedIcon: Icons.explore, 371 - label: 'AT Explorer', 375 + label: l10n.labelAtExplorer, 372 376 isSelected: isDevToolsRoute, 373 377 onTap: () => _pushRoute(context, '/settings/devtools'), 374 378 ), 375 379 _MenuTile( 376 380 icon: Icons.cleaning_services_outlined, 377 381 selectedIcon: Icons.cleaning_services, 378 - label: 'Audit Follows', 382 + label: l10n.labelAuditFollows, 379 383 isSelected: isCleanFollowsRoute, 380 384 onTap: () => _pushRoute(context, '/settings/clean-follows'), 381 385 ), ··· 383 387 _MenuTile( 384 388 icon: Icons.settings_outlined, 385 389 selectedIcon: Icons.settings, 386 - label: 'Settings', 390 + label: l10n.labelSettings, 387 391 isSelected: isSettingsRoute, 388 392 onTap: () => _pushRoute(context, '/settings'), 389 393 ), 390 394 _MenuTile( 391 395 icon: Icons.logout, 392 396 selectedIcon: Icons.logout, 393 - label: 'Log Out', 397 + label: l10n.labelLogOut, 394 398 isDestructive: true, 395 399 onTap: () => 396 400 _runAfterClose(context, () => rootContext.read<AuthBloc>().add(const LogoutRequested())),
+43 -35
lib/features/auth/presentation/login_screen.dart
··· 7 7 import 'package:flutter_svg/flutter_svg.dart'; 8 8 import 'package:go_router/go_router.dart'; 9 9 import 'package:lazurite/core/database/app_database.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/network/app_view_provider.dart'; 12 13 import 'package:lazurite/features/account/cubit/account_switcher_cubit.dart'; ··· 203 204 if (mounted) { 204 205 ScaffoldMessenger.of( 205 206 context, 206 - ).showSnackBar(SnackBar(content: Text('Failed to save provider selection: $error'))); 207 + ).showSnackBar(SnackBar(content: Text(context.l10n.errorFailedToSaveProviderSelection(error)))); 207 208 } 208 209 return false; 209 210 } finally { ··· 224 225 final remove = await showDialog<bool>( 225 226 context: context, 226 227 builder: (dialogContext) => AlertDialog( 227 - title: const Text('Remove Account'), 228 - content: Text('Remove @${account.handle} from this device?'), 228 + title: Text(context.l10n.dialogRemoveAccountTitle), 229 + content: Text(context.l10n.dialogRemoveAccountContent(account.handle)), 229 230 actions: [ 230 - TextButton(onPressed: () => Navigator.pop(dialogContext, false), child: const Text('Cancel')), 231 - FilledButton(onPressed: () => Navigator.pop(dialogContext, true), child: const Text('Remove')), 231 + TextButton(onPressed: () => Navigator.pop(dialogContext, false), child: Text(context.l10n.buttonCancel)), 232 + FilledButton(onPressed: () => Navigator.pop(dialogContext, true), child: Text(context.l10n.buttonRemove)), 232 233 ], 233 234 ), 234 235 ); ··· 242 243 if (!mounted) { 243 244 return; 244 245 } 245 - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Unable to remove account right now.'))); 246 + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.errorUnableToRemoveAccount))); 246 247 return; 247 248 } 248 249 ··· 301 302 Widget build(BuildContext context) { 302 303 final theme = Theme.of(context); 303 304 final colorScheme = theme.colorScheme; 305 + final l10n = context.l10n; 304 306 final accountSwitcherCubit = _maybeAccountSwitcherCubit(context); 305 307 306 308 return Scaffold( ··· 308 310 automaticallyImplyLeading: false, 309 311 actions: [ 310 312 IconButton( 311 - tooltip: 'Settings', 313 + tooltip: l10n.labelSettings, 312 314 icon: const Icon(Icons.settings_outlined), 313 315 onPressed: () => context.push('/settings'), 314 316 ), ··· 347 349 _LogoCard(colorScheme: colorScheme), 348 350 const SizedBox(height: 24), 349 351 Text( 350 - 'Lazurite', 352 + l10n.appTitle, 351 353 textAlign: TextAlign.center, 352 354 style: theme.textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.w700), 353 355 ), 354 356 const SizedBox(height: 8), 355 357 Text( 356 - 'Roam the ATmosphere', 358 + l10n.labelRoamTheAtmosphere, 357 359 textAlign: TextAlign.center, 358 360 style: theme.textTheme.bodyLarge?.copyWith(color: colorScheme.onSurfaceVariant), 359 361 ), ··· 365 367 crossAxisAlignment: CrossAxisAlignment.center, 366 368 children: [ 367 369 Text( 368 - 'Choose your portal', 370 + l10n.labelChooseYourPortal, 369 371 textAlign: TextAlign.center, 370 372 style: theme.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600), 371 373 ), ··· 409 411 debounceMs: 300, 410 412 limit: 8, 411 413 decoration: InputDecoration( 412 - labelText: 'Handle or DID', 413 - hintText: 'username.bsky.social or did:plc:...', 414 + labelText: l10n.promptHandleOrDid, 415 + hintText: l10n.placeholderHandleOrDid, 414 416 prefixIcon: const Icon(Icons.person_outline), 415 417 filled: true, 416 418 fillColor: colorScheme.surface, ··· 424 426 suffixIcon: Padding( 425 427 padding: const EdgeInsets.all(5), 426 428 child: Tooltip( 427 - message: busy ? 'Starting sign in' : 'Continue', 429 + message: busy ? l10n.labelStartingSignIn : l10n.buttonContinue, 428 430 child: Semantics( 429 - label: busy ? 'Starting sign in' : 'Continue sign in', 431 + label: busy ? l10n.labelStartingSignIn : l10n.labelContinueSignIn, 430 432 button: true, 431 433 enabled: !busy, 432 434 child: FilledButton( ··· 496 498 const Expanded(child: Divider()), 497 499 Padding( 498 500 padding: const EdgeInsets.symmetric(horizontal: 12), 499 - child: Text('Debug', style: theme.textTheme.labelMedium), 501 + child: Text(l10n.labelDebug, style: theme.textTheme.labelMedium), 500 502 ), 501 503 const Expanded(child: Divider()), 502 504 ], ··· 512 514 children: [ 513 515 const Icon(Icons.bug_report_outlined), 514 516 const SizedBox(width: 8), 515 - Expanded(child: Text('App Password Login', style: theme.textTheme.titleMedium)), 517 + Expanded( 518 + child: Text(l10n.labelAppPasswordLogin, style: theme.textTheme.titleMedium), 519 + ), 516 520 TextButton( 517 521 onPressed: () { 518 522 setState(() { 519 523 _showDebugForm = !_showDebugForm; 520 524 }); 521 525 }, 522 - child: Text(_showDebugForm ? 'Hide' : 'Show'), 526 + child: Text(_showDebugForm ? l10n.labelHide : l10n.labelShow), 523 527 ), 524 528 ], 525 529 ), ··· 527 531 const SizedBox(height: 12), 528 532 TextFormField( 529 533 controller: _appPasswordController, 530 - decoration: const InputDecoration( 531 - labelText: 'App Password', 532 - hintText: 'xxxx-xxxx-xxxx-xxxx', 533 - prefixIcon: Icon(Icons.lock_outline), 534 - border: OutlineInputBorder(), 534 + decoration: InputDecoration( 535 + labelText: l10n.labelAppPassword, 536 + hintText: l10n.placeholderAppPassword, 537 + prefixIcon: const Icon(Icons.lock_outline), 538 + border: const OutlineInputBorder(), 535 539 ), 536 540 obscureText: true, 537 541 validator: (value) { 538 542 if (_showDebugForm && (value == null || value.trim().isEmpty)) { 539 - return 'Enter your app password'; 543 + return l10n.validationEnterAppPassword; 540 544 } 541 545 return null; 542 546 }, ··· 553 557 unawaited(_onAppPasswordLogin()); 554 558 }, 555 559 icon: const Icon(Icons.login), 556 - label: const Text('Sign In'), 560 + label: Text(l10n.buttonSignIn), 557 561 ); 558 562 }, 559 563 ), 560 564 const SizedBox(height: 8), 561 565 Text( 562 - 'Can be generated via BlueSky\'s App Passwords section at bsky.app.', 566 + l10n.messageAppPasswordGeneratedViaBluesky, 563 567 style: theme.textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), 564 568 textAlign: TextAlign.center, 565 569 ), ··· 575 579 crossAxisAlignment: WrapCrossAlignment.center, 576 580 spacing: 8, 577 581 children: [ 578 - TextButton(onPressed: () => context.push('/terms'), child: const Text('Terms of Service')), 582 + TextButton(onPressed: () => context.push('/terms'), child: Text(l10n.labelTermsOfService)), 579 583 Text('•', style: theme.textTheme.bodySmall), 580 - TextButton(onPressed: () => context.push('/privacy'), child: const Text('Privacy Policy')), 584 + TextButton(onPressed: () => context.push('/privacy'), child: Text(l10n.labelPrivacyPolicy)), 581 585 ], 582 586 ), 583 587 ], ··· 639 643 return Column( 640 644 crossAxisAlignment: CrossAxisAlignment.stretch, 641 645 children: [ 642 - Text('Saved accounts', style: theme.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600)), 646 + Text(context.l10n.labelSavedAccounts, style: theme.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600)), 643 647 const SizedBox(height: 8), 644 648 DecoratedBox( 645 649 decoration: BoxDecoration( ··· 677 681 return Column( 678 682 crossAxisAlignment: CrossAxisAlignment.stretch, 679 683 children: [ 680 - Text('Saved accounts', style: theme.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600)), 684 + Text(context.l10n.labelSavedAccounts, style: theme.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600)), 681 685 const SizedBox(height: 8), 682 686 DecoratedBox( 683 687 decoration: BoxDecoration( ··· 685 689 borderRadius: BorderRadius.circular(14), 686 690 border: Border.all(color: colorScheme.outlineVariant), 687 691 ), 688 - child: const Padding( 689 - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), 692 + child: Padding( 693 + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), 690 694 child: Row( 691 695 children: [ 692 - SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)), 693 - SizedBox(width: 10), 694 - Text('Loading saved accounts...'), 696 + const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)), 697 + const SizedBox(width: 10), 698 + Text(context.l10n.messageLoadingSavedAccounts), 695 699 ], 696 700 ), 697 701 ), ··· 727 731 leading: ProfileAvatar(size: 36, fallbackText: label, imageUrl: snapshot.data), 728 732 title: Text(label, maxLines: 1, overflow: TextOverflow.ellipsis), 729 733 subtitle: Text('@${account.handle}', maxLines: 1, overflow: TextOverflow.ellipsis), 730 - trailing: IconButton(tooltip: 'Remove account', icon: const Icon(Icons.close_rounded), onPressed: onRemove), 734 + trailing: IconButton( 735 + tooltip: context.l10n.labelRemoveAccount, 736 + icon: const Icon(Icons.close_rounded), 737 + onPressed: onRemove, 738 + ), 731 739 onTap: onTap, 732 740 ); 733 741 },
+4 -4
lib/features/moderation/presentation/widgets/moderated_blur_overlay.dart
··· 1 1 import 'dart:ui'; 2 + import 'package:lazurite/core/l10n/l10n.dart'; 2 3 import 'package:lazurite/core/theme/theme_extensions.dart'; 3 4 4 5 import 'package:bluesky/moderation.dart' as bsky_moderation; ··· 37 38 } 38 39 39 40 final colorScheme = context.colorScheme; 41 + final l10n = context.l10n; 40 42 final canReveal = !widget.ui.noOverride; 41 43 final moderationService = maybeModerationService(context); 42 44 final locale = Localizations.localeOf(context); ··· 87 89 ), 88 90 const SizedBox(height: 6), 89 91 Text( 90 - canReveal 91 - ? 'Hidden by your moderation settings. You can reveal it for this view.' 92 - : 'Hidden by your moderation settings and cannot be revealed here.', 92 + canReveal ? l10n.messageModeratedContentCanReveal : l10n.messageModeratedContentCannotReveal, 93 93 textAlign: TextAlign.center, 94 94 style: context.textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant, height: 1.35), 95 95 ), ··· 97 97 const SizedBox(height: 14), 98 98 FilledButton.tonal( 99 99 onPressed: () => setState(() => _revealed = true), 100 - child: const Text('Show content'), 100 + child: Text(l10n.buttonShowContent), 101 101 ), 102 102 ], 103 103 ],
+83 -38
lib/features/search/presentation/search_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/router/app_shell.dart'; 9 10 import 'package:lazurite/core/theme/animation_tokens.dart'; 10 11 import 'package:lazurite/core/theme/animation_utils.dart'; ··· 137 138 Future<void> _onClearHistory() async { 138 139 await showConfirmationDialog( 139 140 context: context, 140 - title: const Text('Clear search history?'), 141 - content: const Text('This will delete all your recent searches.'), 142 - confirmLabel: 'Clear', 141 + title: Text(context.l10n.messageClearSearchHistoryTitle), 142 + content: Text(context.l10n.messageClearSearchHistoryContent), 143 + confirmLabel: context.l10n.buttonClear, 143 144 onConfirmed: () => context.read<SearchBloc>().add(const HistoryCleared()), 144 145 ); 145 146 } ··· 170 171 171 172 final showTypingHint = controller.text.trim().length <= 3; 172 173 return ConfirmationDialog( 173 - title: const Text('Jump to profile'), 174 + title: Text(context.l10n.labelJumpToProfile), 174 175 content: SizedBox( 175 176 width: 420, 176 177 child: Column( ··· 190 191 textInputAction: TextInputAction.search, 191 192 onFieldSubmitted: submitHandle, 192 193 onChanged: (_) => setDialogState(() {}), 193 - decoration: const InputDecoration( 194 - labelText: 'Handle', 194 + decoration: InputDecoration( 195 + labelText: context.l10n.labelHandle, 195 196 hintText: 'alice.bsky.social', 196 197 prefixText: '@', 197 - border: OutlineInputBorder(), 198 + border: const OutlineInputBorder(), 198 199 ), 199 200 ), 200 201 const SizedBox(height: 12), 201 202 if (showTypingHint) 202 203 Align( 203 204 alignment: Alignment.topLeft, 204 - child: Text('Start typing to search handles.', style: context.textTheme.bodySmall), 205 + child: Text(context.l10n.messageStartTypingToSearchHandles, style: context.textTheme.bodySmall), 205 206 ), 206 207 ], 207 208 ), 208 209 ), 209 - confirmLabel: 'Open', 210 + confirmLabel: context.l10n.buttonOpen, 210 211 confirmEnabled: controller.text.trim().isNotEmpty, 211 212 onCancel: () => Navigator.of(dialogContext).pop(), 212 213 onConfirm: submitHandle, ··· 220 221 @override 221 222 Widget build(BuildContext context) { 222 223 final shouldShowFab = widget.showJumpToProfileAction && !widget.postsOnlyMode; 224 + final l10n = context.l10n; 223 225 return AppScreenEntrance( 224 226 child: Scaffold( 225 227 appBar: widget.postsOnlyMode 226 228 ? AppBar( 227 - title: Text(widget.title ?? 'Search Posts'), 229 + title: Text(widget.title ?? l10n.labelSearchPosts), 228 230 leading: widget.showBackButton 229 231 ? IconButton( 230 232 icon: const Icon(Icons.arrow_back), ··· 237 239 ? FloatingActionButton.extended( 238 240 onPressed: _openJumpToProfileDialog, 239 241 icon: const Icon(Icons.person_search), 240 - label: const Text('Jump to profile'), 242 + label: Text(l10n.labelJumpToProfile), 241 243 ).animateIfAllowed( 242 244 context, 243 245 effects: const [ ··· 269 271 Widget _buildSearchBar(BuildContext context, SearchState state) { 270 272 final hasText = _searchController.text.isNotEmpty; 271 273 final theme = Theme.of(context); 274 + final l10n = context.l10n; 272 275 final isSearchDisabled = state.currentTab == SearchTab.starterPacks; 273 276 final fieldFillColor = isSearchDisabled 274 277 ? theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.55) ··· 295 298 textInputAction: TextInputAction.search, 296 299 decoration: InputDecoration( 297 300 hintText: _searchPlaceholderForTab(state.currentTab), 298 - helperText: isSearchDisabled ? 'Starter pack search is not available in the API yet.' : null, 301 + helperText: isSearchDisabled ? l10n.messageStarterPackSearchApiUnavailable : null, 299 302 helperMaxLines: 1, 300 303 prefixIcon: Icon( 301 304 isSearchDisabled ? Icons.block_outlined : Icons.search, ··· 384 387 ), 385 388 ), 386 389 child: Text( 387 - tab.label, 390 + _tabLabel(context, tab), 388 391 textAlign: TextAlign.center, 389 392 style: textStyle, 390 393 maxLines: 1, ··· 408 411 runSpacing: 8, 409 412 crossAxisAlignment: WrapCrossAlignment.center, 410 413 children: [ 411 - Text('Sort by', style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant)), 414 + Text( 415 + context.l10n.labelSortBy, 416 + style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant), 417 + ), 412 418 Container( 413 419 decoration: BoxDecoration( 414 420 color: theme.colorScheme.surfaceContainerHighest, ··· 425 431 OutlinedButton.icon( 426 432 onPressed: () => _openPostFiltersSheet(state.postFilters), 427 433 icon: const Icon(Icons.tune, size: 16), 428 - label: const Text('Filters'), 434 + label: Text(context.l10n.labelFilters), 429 435 ), 430 436 ], 431 437 ), ··· 446 452 borderRadius: BorderRadius.circular(8), 447 453 ), 448 454 child: Text( 449 - sort.label, 455 + _sortLabel(context, sort), 450 456 style: theme.textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w500, color: labelColor), 451 457 ), 452 458 ), ··· 521 527 ? const PostSearchFilters() 522 528 : PostSearchFilters(author: widget.fixedPostAuthor), 523 529 ), 524 - child: const Text('Clear all'), 530 + child: Text(context.l10n.buttonClearAll), 525 531 ), 526 532 ), 527 533 ], ··· 584 590 mainAxisSize: MainAxisSize.min, 585 591 crossAxisAlignment: CrossAxisAlignment.start, 586 592 children: [ 587 - Text('Post filters', style: context.textTheme.titleMedium), 593 + Text(context.l10n.labelPostFilters, style: context.textTheme.titleMedium), 588 594 const SizedBox(height: 12), 589 595 TextField( 590 596 controller: mentionsController, ··· 592 598 enableSuggestions: false, 593 599 smartDashesType: SmartDashesType.disabled, 594 600 smartQuotesType: SmartQuotesType.disabled, 595 - decoration: const InputDecoration(labelText: 'Mentions', hintText: 'did:plc:... or handle'), 601 + decoration: InputDecoration( 602 + labelText: context.l10n.labelMentions, 603 + hintText: 'did:plc:... or handle', 604 + ), 596 605 ), 597 606 const SizedBox(height: 10), 598 607 if (widget.fixedPostAuthor == null) ...[ ··· 602 611 enableSuggestions: false, 603 612 smartDashesType: SmartDashesType.disabled, 604 613 smartQuotesType: SmartQuotesType.disabled, 605 - decoration: const InputDecoration(labelText: 'Author', hintText: 'did:plc:... or handle'), 614 + decoration: InputDecoration( 615 + labelText: context.l10n.labelAuthor, 616 + hintText: 'did:plc:... or handle', 617 + ), 606 618 ), 607 619 const SizedBox(height: 10), 608 620 ] else ...[ 609 621 InputDecorator( 610 - decoration: const InputDecoration(labelText: 'Author (fixed)'), 622 + decoration: InputDecoration(labelText: context.l10n.labelAuthorFixed), 611 623 child: Text(widget.fixedPostAuthor!), 612 624 ), 613 625 const SizedBox(height: 10), ··· 618 630 enableSuggestions: false, 619 631 smartDashesType: SmartDashesType.disabled, 620 632 smartQuotesType: SmartQuotesType.disabled, 621 - decoration: const InputDecoration(labelText: 'Language', hintText: 'en'), 633 + decoration: InputDecoration(labelText: context.l10n.labelLanguage, hintText: 'en'), 622 634 ), 623 635 const SizedBox(height: 10), 624 636 TextField( ··· 627 639 enableSuggestions: false, 628 640 smartDashesType: SmartDashesType.disabled, 629 641 smartQuotesType: SmartQuotesType.disabled, 630 - decoration: const InputDecoration(labelText: 'Domain', hintText: 'example.com'), 642 + decoration: InputDecoration(labelText: context.l10n.labelDomain, hintText: 'example.com'), 631 643 ), 632 644 const SizedBox(height: 10), 633 645 TextField( ··· 636 648 enableSuggestions: false, 637 649 smartDashesType: SmartDashesType.disabled, 638 650 smartQuotesType: SmartQuotesType.disabled, 639 - decoration: const InputDecoration(labelText: 'URL', hintText: 'https://example.com/path'), 651 + decoration: InputDecoration( 652 + labelText: context.l10n.labelUrl, 653 + hintText: 'https://example.com/path', 654 + ), 640 655 ), 641 656 const SizedBox(height: 10), 642 657 TextField( ··· 645 660 enableSuggestions: false, 646 661 smartDashesType: SmartDashesType.disabled, 647 662 smartQuotesType: SmartQuotesType.disabled, 648 - decoration: const InputDecoration(labelText: 'Tags', hintText: '#dart, flutter'), 663 + decoration: InputDecoration(labelText: context.l10n.labelTags, hintText: '#dart, flutter'), 649 664 ), 650 665 const SizedBox(height: 12), 651 666 Row( ··· 661 676 setState(() => since = selected); 662 677 }, 663 678 icon: const Icon(Icons.calendar_today, size: 16), 664 - label: Text(since == null ? 'Since' : formatTimestamp(since!)), 679 + label: Text(since == null ? context.l10n.labelSince : formatTimestamp(since!)), 665 680 ), 666 681 ), 667 682 const SizedBox(width: 8), ··· 676 691 setState(() => until = selected); 677 692 }, 678 693 icon: const Icon(Icons.calendar_today, size: 16), 679 - label: Text(until == null ? 'Until' : formatTimestamp(until!)), 694 + label: Text(until == null ? context.l10n.labelUntil : formatTimestamp(until!)), 680 695 ), 681 696 ), 682 697 ], ··· 684 699 const SizedBox(height: 6), 685 700 Row( 686 701 children: [ 687 - TextButton(onPressed: () => setState(() => since = null), child: const Text('Clear since')), 688 - TextButton(onPressed: () => setState(() => until = null), child: const Text('Clear until')), 702 + TextButton( 703 + onPressed: () => setState(() => since = null), 704 + child: Text(context.l10n.labelClearSince), 705 + ), 706 + TextButton( 707 + onPressed: () => setState(() => until = null), 708 + child: Text(context.l10n.labelClearUntil), 709 + ), 689 710 ], 690 711 ), 691 712 const SizedBox(height: 12), 692 713 Row( 693 714 mainAxisAlignment: MainAxisAlignment.end, 694 715 children: [ 695 - TextButton(onPressed: () => Navigator.of(sheetContext).pop(), child: const Text('Cancel')), 716 + TextButton( 717 + onPressed: () => Navigator.of(sheetContext).pop(), 718 + child: Text(context.l10n.buttonCancel), 719 + ), 696 720 TextButton( 697 721 onPressed: () { 698 722 FocusScope.of(sheetContext).unfocus(); ··· 703 727 ); 704 728 Navigator.of(sheetContext).pop(); 705 729 }, 706 - child: const Text('Clear all'), 730 + child: Text(context.l10n.buttonClearAll), 707 731 ), 708 732 const SizedBox(width: 8), 709 733 FilledButton( ··· 727 751 _updatePostFilters(nextFilters); 728 752 Navigator.of(sheetContext).pop(); 729 753 }, 730 - child: const Text('Apply'), 754 + child: Text(context.l10n.buttonApply), 731 755 ), 732 756 ], 733 757 ), ··· 984 1008 } 985 1009 986 1010 String _searchPlaceholderForTab(SearchTab tab) => switch (tab) { 987 - SearchTab.posts => widget.postsOnlyMode ? 'Search this profile\'s posts' : 'Search posts', 988 - _ => tab.placeholder, 1011 + SearchTab.posts => 1012 + widget.postsOnlyMode 1013 + ? context.l10n.messageSearchThisProfilesPostsPlaceholder 1014 + : context.l10n.messageSearchPostsPlaceholder, 1015 + SearchTab.actors => context.l10n.messageSearchPeoplePlaceholder, 1016 + SearchTab.feeds => context.l10n.messageSearchFeedsPlaceholder, 1017 + SearchTab.starterPacks => context.l10n.messageStarterPackSearchUnavailablePlaceholder, 989 1018 }; 990 1019 991 1020 Widget _buildStarterPacksUnavailableState(BuildContext context) => Center( ··· 996 1025 children: [ 997 1026 Icon(Icons.info_outline, size: 52, color: context.colorScheme.outline), 998 1027 const SizedBox(height: 16), 999 - Text('Starter Pack Search Is Unavailable', style: context.textTheme.titleMedium, textAlign: TextAlign.center), 1028 + Text( 1029 + context.l10n.messageStarterPackSearchUnavailableTitle, 1030 + style: context.textTheme.titleMedium, 1031 + textAlign: TextAlign.center, 1032 + ), 1000 1033 const SizedBox(height: 8), 1001 1034 Text( 1002 - '(Starter Pack Search is not yet implemented in the BlueSky API)', 1035 + context.l10n.messageStarterPackSearchUnavailableBody, 1003 1036 style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceVariant), 1004 1037 textAlign: TextAlign.center, 1005 1038 ), ··· 1007 1040 TextButton.icon( 1008 1041 onPressed: _openStarterPackIssue, 1009 1042 icon: const Icon(Icons.open_in_new), 1010 - label: const Text('Track API progress'), 1043 + label: Text(context.l10n.messageTrackApiProgress), 1011 1044 ), 1012 1045 ], 1013 1046 ), ··· 1017 1050 Future<void> _openStarterPackIssue() async { 1018 1051 final launched = await launchUrl(_starterPackSearchIssueUri, mode: LaunchMode.externalApplication); 1019 1052 if (!launched && mounted) { 1020 - showAppSnackBar(context, 'Could not open issue link.'); 1053 + showAppSnackBar(context, context.l10n.messageCouldNotOpenIssueLink); 1021 1054 } 1022 1055 } 1056 + 1057 + String _tabLabel(BuildContext context, SearchTab tab) => switch (tab) { 1058 + SearchTab.posts => context.l10n.labelPosts, 1059 + SearchTab.actors => context.l10n.labelPeople, 1060 + SearchTab.feeds => context.l10n.labelFeeds, 1061 + SearchTab.starterPacks => context.l10n.labelStarterPacks, 1062 + }; 1063 + 1064 + String _sortLabel(BuildContext context, SearchSort sort) => switch (sort) { 1065 + SearchSort.top => context.l10n.labelTop, 1066 + SearchSort.latest => context.l10n.labelLatest, 1067 + }; 1023 1068 } 1024 1069 1025 1070 class _ActorResultTile extends StatelessWidget {
+121 -116
lib/features/settings/presentation/settings_screen.dart
··· 6 6 import 'package:go_router/go_router.dart'; 7 7 import 'package:lazurite/core/cache/local_cache_maintenance_service.dart'; 8 8 import 'package:lazurite/core/crash_reporting/crash_reporting_service.dart'; 9 + import 'package:lazurite/core/l10n/l10n.dart'; 9 10 import 'package:lazurite/core/network/app_view_provider.dart'; 10 11 import 'package:lazurite/core/network/atproto_host_resolver.dart'; 11 12 import 'package:lazurite/core/network/xrpc_network_interceptor.dart'; ··· 29 30 @override 30 31 Widget build(BuildContext context) { 31 32 final authState = context.watch<AuthBloc>().state; 33 + final l10n = context.l10n; 32 34 final tokens = authState.tokens; 33 35 final showAccountSettings = authState.isAuthenticated && tokens != null; 34 36 final backFallbackRoute = showAccountSettings ? '/' : '/login'; ··· 36 38 return Scaffold( 37 39 appBar: AppBar( 38 40 leading: IconButton( 39 - tooltip: 'Back', 41 + tooltip: l10n.labelBack, 40 42 onPressed: () { 41 43 final router = GoRouter.of(context); 42 44 if (router.canPop()) { ··· 51 53 actions: showAccountSettings 52 54 ? [ 53 55 IconButton( 54 - tooltip: 'Log Out', 56 + tooltip: l10n.labelLogOut, 55 57 onPressed: () { 56 58 context.read<AuthBloc>().add(const LogoutRequested()); 57 59 }, ··· 67 69 builder: (context, switcherState) { 68 70 final authenticatedTokens = tokens; 69 71 final subtitle = switcherState.accounts.length > 1 70 - ? '${switcherState.accounts.length} accounts — tap to switch' 72 + ? l10n.formatAccountsTapToSwitch(switcherState.accounts.length) 71 73 : '@${authenticatedTokens.handle}'; 72 74 73 75 return ListTile( ··· 83 85 }, 84 86 ), 85 87 const SizedBox(height: 24), 86 - _buildSectionHeader(context, 'Appearance'), 88 + _buildSectionHeader(context, l10n.labelAppearance), 87 89 _buildThemeSelector(context), 88 90 const SizedBox(height: 24), 89 - _buildSectionHeader(context, 'Layout'), 91 + _buildSectionHeader(context, l10n.labelLayout), 90 92 _buildLayoutSettings(context), 91 93 if (showAccountSettings) ...[ 92 94 const SizedBox(height: 24), 93 - _buildSectionHeader(context, 'Moderation'), 95 + _buildSectionHeader(context, l10n.labelModeration), 94 96 const _ModerationSettingsPreview(), 95 97 ], 96 98 const SizedBox(height: 24), 97 - _buildSectionHeader(context, 'Search'), 99 + _buildSectionHeader(context, l10n.labelSearch), 98 100 _buildSearchSettings(context, showTypeaheadSettings: showAccountSettings), 99 101 if (showAccountSettings) ...[ 100 102 const SizedBox(height: 24), 101 - _buildSectionHeader(context, 'Account'), 103 + _buildSectionHeader(context, l10n.labelAccount), 102 104 const _AtProtocolConnectionCard(), 103 105 const SizedBox(height: 12), 104 106 _SettingsTile( 105 107 icon: Icons.dynamic_feed_outlined, 106 - title: 'Feeds', 107 - subtitle: 'Manage pinned and saved feeds', 108 + title: l10n.labelFeeds, 109 + subtitle: l10n.messageFeedsSubtitle, 108 110 onTap: () => context.push('/feeds'), 109 111 ), 110 112 _SettingsTile( 111 113 icon: Icons.bookmark_outline, 112 - title: 'Bookmarks & Likes', 113 - subtitle: 'View your bookmarked and liked posts', 114 + title: l10n.labelBookmarksAndLikes, 115 + subtitle: l10n.messageBookmarksAndLikesSubtitle, 114 116 onTap: () => context.push('/bookmarks'), 115 117 ), 116 118 _SettingsTile( 117 119 icon: Icons.videocam_outlined, 118 - title: 'Video Upload Limits', 119 - subtitle: 'Check your daily video quota', 120 + title: l10n.labelVideoUploadLimits, 121 + subtitle: l10n.messageVideoUploadLimitsSubtitle, 120 122 onTap: () => context.push('/settings/video-limits'), 121 123 ), 122 124 const SizedBox(height: 24), 123 - _buildSectionHeader(context, 'Account Maintenance'), 125 + _buildSectionHeader(context, l10n.labelAccountMaintenance), 124 126 _SettingsTile( 125 127 icon: Icons.cleaning_services_outlined, 126 - title: 'Clean Follows', 127 - subtitle: 'Audit and unfollow problematic accounts in bulk', 128 + title: l10n.labelCleanFollows, 129 + subtitle: l10n.messageCleanFollowsSubtitle, 128 130 onTap: () => context.push('/settings/clean-follows'), 129 131 ), 130 132 ], 131 133 const SizedBox(height: 24), 132 - _buildSectionHeader(context, 'Advanced'), 134 + _buildSectionHeader(context, l10n.labelAdvanced), 133 135 _buildAdvancedSettings(context), 134 136 const SizedBox(height: 24), 135 - _buildSectionHeader(context, 'Troubleshooting'), 137 + _buildSectionHeader(context, l10n.labelTroubleshooting), 136 138 _buildTroubleshootingSettings(context), 137 139 const SizedBox(height: 24), 138 140 if (!kReleaseMode || kDebugMode) ...[ 139 - _buildSectionHeader(context, 'Developer'), 141 + _buildSectionHeader(context, l10n.labelDeveloper), 140 142 _buildDeveloperSettings(context), 141 143 const SizedBox(height: 24), 142 144 ], 143 - _buildSectionHeader(context, 'About'), 145 + _buildSectionHeader(context, l10n.labelAbout), 144 146 _SettingsTile( 145 147 icon: Icons.explore_outlined, 146 - title: 'AT Explorer', 148 + title: l10n.labelAtExplorer, 147 149 subtitle: 'View PDS Records', 148 150 onTap: () => context.push('/settings/devtools'), 149 151 ), 150 152 _SettingsTile( 151 153 icon: Icons.info_outline, 152 - title: 'About', 154 + title: l10n.labelAbout, 153 155 subtitle: 'Stormlight Labs', 154 156 onTap: () => context.push('/settings/about'), 155 157 ), 156 158 _SettingsTile( 157 159 icon: Icons.gavel_outlined, 158 - title: 'Terms of Service', 160 + title: l10n.labelTermsOfService, 159 161 subtitle: 'Usage rules and responsibilities', 160 162 onTap: () => context.push('/terms'), 161 163 ), 162 164 _SettingsTile( 163 165 icon: Icons.privacy_tip_outlined, 164 - title: 'Privacy Policy', 166 + title: l10n.labelPrivacyPolicy, 165 167 subtitle: 'How Lazurite handles data', 166 168 onTap: () => context.push('/privacy'), 167 169 ), 168 170 if (showAccountSettings) ...[ 169 171 const SizedBox(height: 24), 170 - _buildSectionHeader(context, 'Danger Zone'), 172 + _buildSectionHeader(context, l10n.labelDangerZone), 171 173 _SettingsTile( 172 174 icon: Icons.logout, 173 - title: 'Log Out', 175 + title: l10n.labelLogOut, 174 176 isDestructive: true, 175 177 onTap: () { 176 178 context.read<AuthBloc>().add(const LogoutRequested()); ··· 193 195 ), 194 196 ); 195 197 196 - Widget _title(BuildContext context) => Text('Settings', style: context.textTheme.titleLarge); 198 + Widget _title(BuildContext context) => Text(context.l10n.labelSettings, style: context.textTheme.titleLarge); 197 199 198 200 Widget _buildThemeSelector(BuildContext context) { 199 201 final settingsCubit = context.read<SettingsCubit>(); 200 202 final theme = Theme.of(context); 203 + final l10n = context.l10n; 201 204 202 205 return BlocBuilder<SettingsCubit, SettingsState>( 203 206 builder: (context, state) { ··· 219 222 selectedBackgroundColor: theme.colorScheme.primary, 220 223 selectedForegroundColor: theme.colorScheme.onPrimary, 221 224 ), 222 - segments: const [ 223 - ButtonSegment(value: _AppearanceMode.system, label: Text('System')), 224 - ButtonSegment(value: _AppearanceMode.light, label: Text('Light')), 225 - ButtonSegment(value: _AppearanceMode.dark, label: Text('Dark')), 225 + segments: [ 226 + ButtonSegment(value: _AppearanceMode.system, label: Text(l10n.labelSystem)), 227 + ButtonSegment(value: _AppearanceMode.light, label: Text(l10n.labelLight)), 228 + ButtonSegment(value: _AppearanceMode.dark, label: Text(l10n.labelDark)), 226 229 ], 227 230 selected: {_AppearanceMode.fromState(state)}, 228 231 onSelectionChanged: (selected) { ··· 247 250 child: Align( 248 251 alignment: Alignment.centerLeft, 249 252 child: Text( 250 - 'THEME', 253 + l10n.labelTheme, 251 254 style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600, letterSpacing: 0.5), 252 255 ), 253 256 ), ··· 269 272 Widget _buildLayoutSettings(BuildContext context) { 270 273 final settingsCubit = context.read<SettingsCubit>(); 271 274 final theme = Theme.of(context); 275 + final l10n = context.l10n; 272 276 273 277 return BlocBuilder<SettingsCubit, SettingsState>( 274 278 builder: (context, state) { ··· 283 287 child: Column( 284 288 children: [ 285 289 _SettingsDropdownTile<FeedLayout>( 286 - title: 'Feed Layout', 290 + title: l10n.labelFeedLayout, 287 291 value: state.feedLayout, 288 292 options: FeedLayout.values, 289 293 labelBuilder: (layout) => switch (layout) { 290 - FeedLayout.card => 'Card', 291 - FeedLayout.compact => 'Compact', 294 + FeedLayout.card => l10n.messageFeedLayoutCard, 295 + FeedLayout.compact => l10n.messageFeedLayoutCompact, 292 296 }, 293 297 onChanged: (value) { 294 298 if (value != null) { ··· 298 302 ), 299 303 const Divider(height: 1), 300 304 _SettingsDropdownTile<int?>( 301 - title: 'Thread Auto-Collapse', 302 - subtitle: 'Collapse reply branches deeper than the selected level', 305 + title: l10n.labelThreadAutoCollapse, 306 + subtitle: l10n.messageThreadAutoCollapseSubtitle, 303 307 value: state.threadAutoCollapseDepth, 304 308 options: const <int?>[null, 1, 2, 3, 4, 5, 6], 305 - labelBuilder: (depth) => depth == null ? 'Off' : 'Depth $depth', 309 + labelBuilder: (depth) => depth == null ? l10n.commonOff : l10n.formatDepth(depth), 306 310 onChanged: settingsCubit.setThreadAutoCollapseDepth, 307 311 ), 308 312 const Divider(height: 1), 309 313 _SettingsTile( 310 314 icon: Icons.motion_photos_off_outlined, 311 - title: 'Animations', 312 - subtitle: 'Turn off non-essential motion effects', 315 + title: l10n.labelAnimations, 316 + subtitle: l10n.messageTurnOffNonEssentialMotion, 313 317 trailing: Switch.adaptive( 314 318 value: state.animationsEnabled, 315 319 onChanged: settingsCubit.setAnimationsEnabled, ··· 326 330 BlocBuilder<SettingsCubit, SettingsState>( 327 331 builder: (context, settingsState) { 328 332 final theme = Theme.of(context); 333 + final l10n = context.l10n; 329 334 return Container( 330 335 decoration: BoxDecoration( 331 336 border: Border( ··· 339 344 if (showTypeaheadSettings) ...[ 340 345 ListTile( 341 346 leading: const Icon(Icons.tune_outlined), 342 - title: const Text('Typeahead Provider'), 347 + title: Text(l10n.labelTypeaheadProvider), 343 348 subtitle: Text( 344 349 settingsState.typeaheadProvider == 'community' 345 - ? 'Community (waow.tech) selected. Third-party service.' 346 - : 'Bluesky official endpoint selected.', 350 + ? l10n.messageCommunityTypeaheadSelected 351 + : l10n.messageBlueskyEndpointSelected, 347 352 ), 348 353 ), 349 354 Padding( ··· 351 356 child: Align( 352 357 alignment: Alignment.centerLeft, 353 358 child: SegmentedButton<String>( 354 - segments: const [ 355 - ButtonSegment<String>(value: 'bluesky', label: Text('Bluesky')), 356 - ButtonSegment<String>(value: 'community', label: Text('Community')), 359 + segments: [ 360 + const ButtonSegment<String>(value: 'bluesky', label: Text('Bluesky')), 361 + ButtonSegment<String>(value: 'community', label: Text(l10n.labelCommunity)), 357 362 ], 358 363 selected: {settingsState.typeaheadProvider}, 359 364 onSelectionChanged: (selection) { ··· 364 369 ), 365 370 const Divider(height: 1), 366 371 ], 367 - const _SettingsTile( 372 + _SettingsTile( 368 373 icon: Icons.manage_search_outlined, 369 - title: 'Semantic Search', 370 - subtitle: 'Manage semantic search from Bookmarks & Likes -> Search', 374 + title: l10n.labelSemanticSearch, 375 + subtitle: l10n.messageManageSemanticSearchSubtitle, 371 376 ), 372 377 ], 373 378 ), ··· 378 383 Widget _buildDeveloperSettings(BuildContext context) { 379 384 final settingsCubit = context.read<SettingsCubit>(); 380 385 final crashReportingService = _readCrashReportingServiceOrNull(context); 386 + final l10n = context.l10n; 381 387 382 388 return BlocBuilder<SettingsCubit, SettingsState>( 383 389 builder: (context, state) { ··· 394 400 children: [ 395 401 _SettingsTile( 396 402 icon: Icons.cloud_off_outlined, 397 - title: 'Go Offline', 398 - subtitle: 'Turn off online connectivity', 403 + title: l10n.labelGoOffline, 404 + subtitle: l10n.messageDeveloperGoOfflineSubtitle, 399 405 trailing: Switch.adaptive(value: state.simulateOffline, onChanged: settingsCubit.setSimulateOffline), 400 406 ), 401 407 const Divider(height: 1), 402 408 _SettingsTile( 403 409 icon: Icons.bug_report_outlined, 404 - title: 'Crashlytics Test Crash', 405 - subtitle: 'Intentionally crash to validate Crashlytics reports', 410 + title: l10n.labelCrashlyticsTestCrash, 411 + subtitle: l10n.messageCrashlyticsTestCrashSubtitle, 406 412 trailing: const Icon(Icons.warning_amber_rounded), 407 413 onTap: crashReportingService?.crash, 408 414 ), ··· 410 416 const Divider(height: 1), 411 417 _SettingsTile( 412 418 icon: Icons.lock_reset_outlined, 413 - title: 'Force Next XRPC 401', 414 - subtitle: 'Debug-only: next network request returns Unauthorized to test token refresh', 419 + title: l10n.labelForceNextXrpc401, 420 + subtitle: l10n.messageForceNextXrpc401Subtitle, 415 421 trailing: const Icon(Icons.play_arrow_outlined), 416 422 onTap: () { 417 423 XrpcNetworkInterceptor.debugForceUnauthorizedOnce(); 418 - showAppSnackBar(context, 'Armed: next XRPC request will return debug 401 Unauthorized'); 424 + showAppSnackBar(context, l10n.messageAppViewDebug401Armed); 419 425 }, 420 426 ), 421 427 ], ··· 437 443 Widget _buildAdvancedSettings(BuildContext context) { 438 444 final settingsCubit = context.read<SettingsCubit>(); 439 445 final theme = Theme.of(context); 446 + final l10n = context.l10n; 440 447 return BlocBuilder<SettingsCubit, SettingsState>( 441 448 builder: (context, state) { 442 449 return Container( ··· 451 458 children: [ 452 459 _SettingsTile( 453 460 icon: Icons.description_outlined, 454 - title: 'Logs', 461 + title: l10n.labelLogs, 455 462 subtitle: 'View app log files', 456 463 onTap: () => context.push('/settings/logs'), 457 464 ), ··· 460 467 const Divider(height: 1), 461 468 _SettingsTile( 462 469 icon: Icons.route_outlined, 463 - title: 'AppView Provider', 464 - subtitle: _appViewSubtitle(state.appViewProvider), 470 + title: l10n.labelAppViewProvider, 471 + subtitle: _appViewSubtitle(context, state.appViewProvider), 465 472 ), 466 473 Padding( 467 474 padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), ··· 487 494 const Divider(height: 1), 488 495 _SettingsTile( 489 496 icon: Icons.compare_arrows_outlined, 490 - title: 'Cross-Provider Fallback', 491 - subtitle: 'Retry public reads on the alternate AppView when transient errors occur', 497 + title: l10n.labelCrossProviderFallback, 498 + subtitle: l10n.messageCrossProviderFallbackSubtitle, 492 499 trailing: Switch.adaptive( 493 500 value: state.crossProviderFallbackEnabled, 494 501 onChanged: settingsCubit.setCrossProviderFallbackEnabled, ··· 497 504 const Divider(height: 1), 498 505 _SettingsTile( 499 506 icon: Icons.alt_route_outlined, 500 - title: 'Slingshot Identity Fallback', 501 - subtitle: 'If handle lookup fails, use Slingshot to find your DID and PDS so sign-in can continue', 507 + title: l10n.labelSlingshotIdentityFallback, 508 + subtitle: l10n.messageSlingshotIdentityFallbackSubtitle, 502 509 trailing: Switch.adaptive( 503 510 value: state.slingshotIdentityFallbackEnabled, 504 511 onChanged: settingsCubit.setSlingshotIdentityFallbackEnabled, ··· 507 514 const Divider(height: 1), 508 515 _SettingsTile( 509 516 icon: Icons.bug_report_outlined, 510 - title: 'Crash Reporting', 517 + title: l10n.labelCrashReporting, 511 518 subtitle: state.crashReportingEnabled 512 - ? 'Enabled. Crash and error reports are sent to improve stability.' 513 - : 'Disabled. Crash and error reports are not sent.', 519 + ? l10n.messageCrashReportingEnabled 520 + : l10n.messageCrashReportingDisabled, 514 521 trailing: Switch.adaptive( 515 522 value: state.crashReportingEnabled, 516 523 onChanged: (enabled) => unawaited(_handleCrashReportingToggle(context, enabled)), 517 524 ), 518 525 ), 519 526 const Divider(height: 1), 520 - const _SettingsTile( 527 + _SettingsTile( 521 528 icon: Icons.monitor_heart_outlined, 522 - title: 'Provider Diagnostics', 523 - subtitle: 'Moderation/ranking can differ by provider. Verify health and recent fallback state.', 529 + title: l10n.labelProviderDiagnostics, 530 + subtitle: l10n.messageProviderDiagnosticsSubtitle, 524 531 ), 525 532 _ConnectionDetailRow( 526 - label: 'Active Provider', 533 + label: l10n.labelActiveProvider, 527 534 value: AppViewProviders.providerDisplayName(state.appViewProvider), 528 535 ), 529 536 const Divider(height: 1), 530 - _ConnectionDetailRow(label: 'Health', value: state.appViewHealthSummary ?? 'Not checked yet'), 537 + _ConnectionDetailRow( 538 + label: l10n.labelHealth, 539 + value: state.appViewHealthSummary ?? l10n.commonNotCheckedYet, 540 + ), 531 541 const Divider(height: 1), 532 542 _ConnectionDetailRow( 533 - label: 'Last Health Check', 543 + label: l10n.labelLastHealthCheck, 534 544 value: state.appViewHealthCheckedAt == null 535 - ? 'Never' 545 + ? l10n.commonNever 536 546 : formatTimestamp(state.appViewHealthCheckedAt!.toLocal()), 537 547 ), 538 548 const Divider(height: 1), 539 - _ConnectionDetailRow(label: 'Last Fallback', value: state.appViewLastFallback ?? 'None'), 549 + _ConnectionDetailRow(label: l10n.labelLastFallback, value: state.appViewLastFallback ?? l10n.commonNone), 540 550 const Divider(height: 1), 541 - _ConnectionDetailRow(label: 'Last Error', value: state.appViewLastError ?? 'None'), 551 + _ConnectionDetailRow(label: l10n.labelLastError, value: state.appViewLastError ?? l10n.commonNone), 542 552 const Divider(height: 1), 543 553 _SettingsTile( 544 554 icon: Icons.medical_information_outlined, 545 - title: 'Refresh Provider Health', 546 - subtitle: 'Probe public AppView endpoints now', 555 + title: l10n.labelRefreshProviderHealth, 556 + subtitle: l10n.messageRefreshProviderHealthSubtitle, 547 557 trailing: state.appViewHealthRefreshing 548 558 ? const SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2)) 549 559 : const Icon(Icons.refresh_outlined), ··· 560 570 ); 561 571 } 562 572 563 - String _appViewSubtitle(String providerKey) { 573 + String _appViewSubtitle(BuildContext context, String providerKey) { 564 574 final provider = AppViewProviders.providerDisplayName(providerKey); 565 - return '$provider selected. Switching providers performs a soft restart.'; 575 + return context.l10n.formatAppViewProviderSelected(provider); 566 576 } 567 577 568 578 Future<void> _confirmAndApplyProviderChange(BuildContext context, String selectedProvider) async { ··· 570 580 context: context, 571 581 builder: (dialogContext) { 572 582 return AlertDialog( 573 - title: const Text('Switch AppView provider?'), 574 - content: const Text( 575 - 'Apply and restart now to rebuild network services.\n\n' 576 - 'You will stay signed in and no local data will be deleted.\n\n' 577 - 'Moderation labels, ranking, and trending results can differ between providers.', 578 - ), 583 + title: Text(context.l10n.dialogSwitchAppViewProviderTitle), 584 + content: Text(context.l10n.dialogSwitchAppViewProviderContent), 579 585 actions: [ 580 - TextButton(onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text('Cancel')), 586 + TextButton(onPressed: () => Navigator.of(dialogContext).pop(false), child: Text(context.l10n.buttonCancel)), 581 587 FilledButton( 582 588 onPressed: () => Navigator.of(dialogContext).pop(true), 583 - child: const Text('Apply and Restart'), 589 + child: Text(context.l10n.buttonApplyAndRestart), 584 590 ), 585 591 ], 586 592 ); ··· 609 615 610 616 Widget _buildTroubleshootingSettings(BuildContext context) { 611 617 final theme = Theme.of(context); 618 + final l10n = context.l10n; 612 619 return Container( 613 620 decoration: BoxDecoration( 614 621 border: Border( ··· 621 628 children: [ 622 629 _SettingsTile( 623 630 icon: Icons.cached_outlined, 624 - title: 'Clear Cache', 625 - subtitle: 'Remove cached posts, profiles, images, feeds, threads, and semantic search data', 631 + title: l10n.labelClearCache, 632 + subtitle: l10n.messageClearCacheSubtitle, 626 633 onTap: () => unawaited(_confirmAndClearCaches(context)), 627 634 ), 628 635 const Divider(height: 1), 629 636 _SettingsTile( 630 637 icon: Icons.manage_accounts_outlined, 631 - title: 'Reset Sign-In Data', 632 - subtitle: 'Troubleshoot OAuth or account-switching issues by clearing local sessions on this device', 638 + title: l10n.labelResetSignInData, 639 + subtitle: l10n.messageResetSignInDataSubtitle, 633 640 isDestructive: true, 634 641 onTap: () => unawaited(_confirmAndClearLocalAuthData(context)), 635 642 ), ··· 643 650 context: context, 644 651 builder: (dialogContext) { 645 652 return AlertDialog( 646 - title: const Text('Clear cache?'), 647 - content: const Text( 648 - 'This removes cached posts, profiles, images, feeds, threads, label data, and local semantic search data.\n\n' 649 - 'Accounts, settings, drafts, bookmarks, and likes are kept.', 650 - ), 653 + title: Text(context.l10n.dialogClearCacheTitle), 654 + content: Text(context.l10n.dialogClearCacheContent), 651 655 actions: [ 652 - TextButton(onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text('Cancel')), 653 - FilledButton(onPressed: () => Navigator.of(dialogContext).pop(true), child: const Text('Clear Cache')), 656 + TextButton(onPressed: () => Navigator.of(dialogContext).pop(false), child: Text(context.l10n.buttonCancel)), 657 + FilledButton( 658 + onPressed: () => Navigator.of(dialogContext).pop(true), 659 + child: Text(context.l10n.buttonClearCache), 660 + ), 654 661 ], 655 662 ); 656 663 }, ··· 663 670 try { 664 671 await context.read<LocalCacheMaintenanceService>().clearCaches(); 665 672 if (context.mounted) { 666 - showAppSnackBar(context, 'Cache cleared'); 673 + showAppSnackBar(context, context.l10n.labelCacheCleared); 667 674 } 668 675 } catch (error) { 669 676 if (context.mounted) { 670 - showAppSnackBar(context, 'Failed to clear cache: $error', isError: true); 677 + showAppSnackBar(context, context.l10n.errorFailedToClearCache(error), isError: true); 671 678 } 672 679 } 673 680 } ··· 677 684 context: context, 678 685 builder: (dialogContext) { 679 686 return AlertDialog( 680 - title: const Text('Reset sign-in data?'), 681 - content: const Text( 682 - 'Use this only when troubleshooting sign-in or account switching.\n\n' 683 - 'This clears all local account sessions on this device and sends you back to sign in. ' 684 - 'It does not delete your Bluesky account or posts.', 685 - ), 687 + title: Text(context.l10n.dialogResetSignInDataTitle), 688 + content: Text(context.l10n.dialogResetSignInDataContent), 686 689 actions: [ 687 - TextButton(onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text('Cancel')), 690 + TextButton(onPressed: () => Navigator.of(dialogContext).pop(false), child: Text(context.l10n.buttonCancel)), 688 691 FilledButton( 689 692 style: FilledButton.styleFrom( 690 693 backgroundColor: Theme.of(dialogContext).colorScheme.error, 691 694 foregroundColor: Theme.of(dialogContext).colorScheme.onError, 692 695 ), 693 696 onPressed: () => Navigator.of(dialogContext).pop(true), 694 - child: const Text('Reset Sign-In Data'), 697 + child: Text(context.l10n.buttonResetSignInData), 695 698 ), 696 699 ], 697 700 ); ··· 744 747 @override 745 748 Widget build(BuildContext context) { 746 749 final service = _service; 750 + final l10n = context.l10n; 747 751 if (service == null) { 748 752 return _SettingsTile( 749 753 icon: Icons.shield_outlined, 750 - title: 'Content Moderation', 751 - subtitle: 'Manage labelers and visibility rules', 754 + title: l10n.labelContentModeration, 755 + subtitle: l10n.messageContentModerationSubtitle, 752 756 onTap: () => context.push('/settings/moderation'), 753 757 ); 754 758 } ··· 765 769 children: [ 766 770 _SettingsTile( 767 771 icon: Icons.visibility_outlined, 768 - title: 'Adult Content', 769 - subtitle: adultEnabled ? '18+ labels can be configured' : 'Required before 18+ labels can be configured', 772 + title: l10n.labelAdultContent, 773 + subtitle: adultEnabled ? l10n.messageAdultContentEnabled : l10n.messageAdultContentRequired, 770 774 trailing: Switch.adaptive(value: adultEnabled, onChanged: _isUpdating ? null : _toggleAdultContent), 771 775 ), 772 776 const Divider(height: 1), 773 777 _SettingsTile( 774 778 icon: Icons.policy_outlined, 775 - title: 'Content Moderation', 776 - subtitle: '$customLabelers custom labeler${customLabelers == 1 ? '' : 's'} subscribed', 779 + title: l10n.labelContentModeration, 780 + subtitle: l10n.formatContentModerationCustomLabelers(customLabelers), 777 781 onTap: () => context.push('/settings/moderation'), 778 782 ), 779 783 ], ··· 797 801 798 802 final pds = resolvePdsHost(tokens); 799 803 final theme = Theme.of(context); 804 + final l10n = context.l10n; 800 805 return Container( 801 806 decoration: BoxDecoration( 802 807 border: Border( ··· 811 816 children: [ 812 817 Padding( 813 818 padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), 814 - child: Text('AT Protocol Connection', style: context.textTheme.titleMedium), 819 + child: Text(l10n.labelAtProtocolConnection, style: context.textTheme.titleMedium), 815 820 ), 816 821 const Divider(height: 1), 817 822 _ConnectionDetailRow(label: 'Handle', value: '@${tokens.handle}'),
+7 -3
lib/main.dart
··· 14 14 import 'package:lazurite/core/crash_reporting/crash_reporting_service.dart'; 15 15 import 'package:lazurite/core/database/app_database.dart'; 16 16 import 'package:lazurite/core/embedding/embedding_service.dart'; 17 + import 'package:lazurite/core/l10n/app_localizations.dart'; 17 18 import 'package:lazurite/core/logging/app_logger.dart'; 18 19 import 'package:lazurite/core/logging/logging_bloc_observer.dart'; 19 20 import 'package:lazurite/core/logging/logging_navigator_observer.dart'; ··· 45 46 import 'package:lazurite/features/messages/bloc/convo_list_bloc.dart'; 46 47 import 'package:lazurite/features/messages/data/convo_repository.dart'; 47 48 import 'package:lazurite/features/moderation/data/moderation_service.dart'; 49 + import 'package:lazurite/features/notifications/background/notification_background_worker.dart'; 50 + import 'package:lazurite/features/notifications/data/firebase_push_token_provider.dart'; 51 + import 'package:lazurite/features/notifications/data/flutter_local_notification_adapter.dart'; 48 52 import 'package:lazurite/features/notifications/data/notification_repository.dart'; 49 - import 'package:lazurite/features/notifications/data/flutter_local_notification_adapter.dart'; 50 - import 'package:lazurite/features/notifications/data/firebase_push_token_provider.dart'; 51 53 import 'package:lazurite/features/notifications/domain/local_notification_adapter.dart'; 52 54 import 'package:lazurite/features/notifications/domain/notification_deep_link_navigator.dart'; 53 55 import 'package:lazurite/features/notifications/domain/notification_domain_service.dart'; 54 56 import 'package:lazurite/features/notifications/domain/notification_local_models.dart'; 55 57 import 'package:lazurite/features/notifications/domain/push_registration_service.dart'; 56 - import 'package:lazurite/features/notifications/background/notification_background_worker.dart'; 57 58 import 'package:lazurite/features/profile/bloc/profile_bloc.dart'; 58 59 import 'package:lazurite/features/profile/data/profile_action_repository.dart'; 59 60 import 'package:lazurite/features/profile/data/profile_repository.dart'; ··· 507 508 return MaterialApp.router( 508 509 key: ValueKey('router-$_routerSessionKey-$_routerGeneration'), 509 510 title: 'Lazurite', 511 + onGenerateTitle: (context) => AppLocalizations.of(context).appTitle, 510 512 debugShowCheckedModeBanner: false, 511 513 theme: lightTheme, 512 514 darkTheme: darkTheme, 513 515 themeMode: themeMode, 516 + localizationsDelegates: AppLocalizations.localizationsDelegates, 517 + supportedLocales: AppLocalizations.supportedLocales, 514 518 routerConfig: _router, 515 519 builder: (context, child) => GlobalTapOutsideUnfocus( 516 520 child: Stack(
+9 -5
lib/shared/presentation/widgets/confirmation_dialog.dart
··· 1 1 import 'dart:async'; 2 - import 'package:lazurite/core/theme/theme_extensions.dart'; 3 2 4 3 import 'package:flutter/material.dart'; 4 + import 'package:lazurite/core/l10n/l10n.dart'; 5 + import 'package:lazurite/core/theme/theme_extensions.dart'; 5 6 6 7 class ConfirmationDialog extends StatelessWidget { 7 8 const ConfirmationDialog({ ··· 10 11 required this.content, 11 12 required this.confirmLabel, 12 13 required this.onConfirm, 13 - this.cancelLabel = 'Cancel', 14 + this.cancelLabel, 14 15 this.onCancel, 15 16 this.confirmDestructive = false, 16 17 this.showCancel = true, ··· 21 22 final Widget content; 22 23 final String confirmLabel; 23 24 final VoidCallback onConfirm; 24 - final String cancelLabel; 25 + final String? cancelLabel; 25 26 final VoidCallback? onCancel; 26 27 final bool confirmDestructive; 27 28 final bool showCancel; ··· 34 35 content: content, 35 36 actions: [ 36 37 if (showCancel) 37 - TextButton(onPressed: onCancel ?? () => Navigator.of(context).pop(false), child: Text(cancelLabel)), 38 + TextButton( 39 + onPressed: onCancel ?? () => Navigator.of(context).pop(false), 40 + child: Text(cancelLabel ?? context.l10n.buttonCancel), 41 + ), 38 42 FilledButton( 39 43 onPressed: confirmEnabled ? onConfirm : null, 40 44 style: confirmDestructive ··· 55 59 required Widget title, 56 60 required Widget content, 57 61 required String confirmLabel, 58 - String cancelLabel = 'Cancel', 62 + String? cancelLabel, 59 63 bool confirmDestructive = false, 60 64 bool showCancel = true, 61 65 bool barrierDismissible = true,
+8 -6
lib/shared/presentation/widgets/error_state.dart
··· 1 1 import 'package:flutter/material.dart'; 2 + import 'package:lazurite/core/l10n/l10n.dart'; 2 3 3 4 class ErrorState extends StatelessWidget { 4 5 const ErrorState({ 5 6 super.key, 6 7 required this.message, 7 8 required this.onRetry, 8 - this.title = 'Something went wrong', 9 - this.retryLabel = 'Retry', 9 + this.title, 10 + this.retryLabel, 10 11 this.icon = Icons.error_outline, 11 12 }); 12 13 13 - final String title; 14 + final String? title; 14 15 final String message; 15 16 final VoidCallback onRetry; 16 - final String retryLabel; 17 + final String? retryLabel; 17 18 final IconData icon; 18 19 19 20 @override 20 21 Widget build(BuildContext context) { 21 22 final theme = Theme.of(context); 23 + final l10n = context.l10n; 22 24 return Center( 23 25 child: Padding( 24 26 padding: const EdgeInsets.all(24), ··· 27 29 children: [ 28 30 Icon(icon, size: 48, color: theme.colorScheme.outline), 29 31 const SizedBox(height: 12), 30 - Text(title, style: theme.textTheme.titleMedium, textAlign: TextAlign.center), 32 + Text(title ?? l10n.errorGenericTitle, style: theme.textTheme.titleMedium, textAlign: TextAlign.center), 31 33 const SizedBox(height: 8), 32 34 Text(message, textAlign: TextAlign.center), 33 35 const SizedBox(height: 16), 34 - FilledButton(onPressed: onRetry, child: Text(retryLabel)), 36 + FilledButton(onPressed: onRetry, child: Text(retryLabel ?? l10n.buttonRetry)), 35 37 ], 36 38 ), 37 39 ),
+6 -4
lib/shared/utils/format_utils.dart
··· 14 14 } 15 15 16 16 /// Formats counts using compact K/M suffixes. 17 - String formatCount(int count) { 17 + String formatCount(int count, {String? locale}) { 18 + final numberFormat = NumberFormat.decimalPattern(locale ?? Intl.getCurrentLocale()); 18 19 final absoluteCount = count.abs(); 19 20 final sign = count < 0 ? '-' : ''; 20 21 if (absoluteCount >= 1000000) { ··· 23 24 if (absoluteCount >= 1000) { 24 25 return '$sign${(absoluteCount / 1000).toStringAsFixed(1)}K'; 25 26 } 26 - return '$count'; 27 + return numberFormat.format(count); 27 28 } 28 29 29 30 /// Formats a relative timestamp using short units with optional suffix/casing. ··· 33 34 String nowLabel = 'now', 34 35 bool includeAgo = false, 35 36 bool uppercase = false, 37 + String? locale, 36 38 }) { 37 39 final current = now ?? DateTime.now(); 38 40 var difference = current.difference(time); ··· 46 48 final d when d.inHours < 1 => '${d.inMinutes}m$agoSuffix', 47 49 final d when d.inDays < 1 => '${d.inHours}h$agoSuffix', 48 50 final d when d.inDays < 7 => '${d.inDays}d$agoSuffix', 49 - _ => DateFormat('MMM d').format(time), 51 + _ => DateFormat('MMM d', locale ?? Intl.getCurrentLocale()).format(time), 50 52 }; 51 53 52 54 return uppercase ? formatted.toUpperCase() : formatted; 53 55 } 54 56 55 - String formatTimestamp(DateTime time) { 57 + String formatTimestamp(DateTime time, {String? locale}) { 56 58 final month = time.month.toString().padLeft(2, '0'); 57 59 final day = time.day.toString().padLeft(2, '0'); 58 60 final hour = time.hour.toString().padLeft(2, '0');
+7 -2
pubspec.lock
··· 630 630 url: "https://pub.dev" 631 631 source: hosted 632 632 version: "1.0.3" 633 + flutter_localizations: 634 + dependency: "direct main" 635 + description: flutter 636 + source: sdk 637 + version: "0.0.0" 633 638 flutter_plugin_android_lifecycle: 634 639 dependency: transitive 635 640 description: ··· 852 857 dependency: "direct main" 853 858 description: 854 859 name: intl 855 - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf 860 + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" 856 861 url: "https://pub.dev" 857 862 source: hosted 858 - version: "0.19.0" 863 + version: "0.20.2" 859 864 io: 860 865 dependency: transitive 861 866 description:
+4 -1
pubspec.yaml
··· 9 9 dependencies: 10 10 flutter: 11 11 sdk: flutter 12 + flutter_localizations: 13 + sdk: flutter 12 14 cupertino_icons: ^1.0.8 13 15 atproto: ^1.4.1 14 16 atproto_core: ^1.2.0 ··· 26 28 json_annotation: ^4.9.0 27 29 crypto: ^3.0.6 28 30 url_launcher: ^6.3.1 29 - intl: ^0.19.0 31 + intl: ^0.20.2 30 32 google_fonts: ^8.0.2 31 33 logger: ^2.6.2 32 34 share_plus: ^10.1.4 ··· 74 76 75 77 flutter: 76 78 uses-material-design: true 79 + generate: true 77 80 78 81 # Vocab Text: https://huggingface.co/google-bert/bert-base-uncased/blob/main/vocab.txt 79 82 # MiniLM Model: https://huggingface.co/Nihal2000/all-MiniLM-L6-v2-quant.tflite/tree/main
+36
test/core/l10n/app_localizations_test.dart
··· 1 + import 'package:flutter/material.dart'; 2 + import 'package:flutter_test/flutter_test.dart'; 3 + import 'package:lazurite/core/l10n/app_localizations.dart'; 4 + import 'package:lazurite/core/l10n/l10n.dart'; 5 + 6 + void main() { 7 + testWidgets('is available from MaterialApp localization delegates', (tester) async { 8 + await tester.pumpWidget( 9 + MaterialApp( 10 + localizationsDelegates: AppLocalizations.localizationsDelegates, 11 + supportedLocales: AppLocalizations.supportedLocales, 12 + home: Builder( 13 + builder: (context) { 14 + return Text(context.l10n.appTitle); 15 + }, 16 + ), 17 + ), 18 + ); 19 + 20 + expect(find.text('Lazurite'), findsOneWidget); 21 + }); 22 + 23 + testWidgets('context helper falls back to English in minimal widget tests', (tester) async { 24 + await tester.pumpWidget( 25 + MaterialApp( 26 + home: Builder( 27 + builder: (context) { 28 + return Text(context.l10n.buttonRetry); 29 + }, 30 + ), 31 + ), 32 + ); 33 + 34 + expect(find.text('Retry'), findsOneWidget); 35 + }); 36 + }