diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 51247ccd..11890cdc 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -14,6 +14,7 @@ Thank you for your interest in contributing to Kover. This document outlines the
- [Examples](#examples)
- [Reporting Issues](#reporting-issues)
- [Requesting Features & Enhancements](#requesting-features-enhancements)
+ - [Localizations](#localizations)
- [Developing Kover](#developing-kover)
- [Codebase Overview](#codebase-overview)
- [Architecture](#architecture)
@@ -73,6 +74,13 @@ If an idea is not yet clearly defined, consider starting a discussion instead of
---
+## Localizations
+
+Localization contributions are also very welcome and a great way to contribute to Kover. A Weblate project is available
+to anyone interested in increasing Kover's language support. To get started, please visit the project on [Weblate](https://hosted.weblate.org/engage/kover/)
+
+---
+
## Developing Kover
### Codebase Overview
@@ -134,6 +142,8 @@ graph LR
- Avoid manually modifying generated code.
- Avoid violating the architecture layer boundaries.
- For example, UI code should not directly access the database or API, but rather go through providers and managers.
+- New text strings exposed in the UI should be added to the English localization file `lib/l10n/en.arb` and accompanied by a description for the string in
+ the respective `@` entry. No other localization files should be manually edited as those are managed through Weblate.
### Setting Up Your Development Environment
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 263fbd57..c29c46ed 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -52,7 +52,9 @@ jobs:
run: cd macos && fastlane disable_codesign
- name: Code Generation 🧬
- run: dart run build_runner build --delete-conflicting-outputs
+ run: |
+ dart run build_runner build --delete-conflicting-outputs
+ flutter gen-l10n
- name: Validate DB Schema 🗄️
if: matrix.platform != 'windows'
diff --git a/.gitignore b/.gitignore
index 06c97231..0107d5cc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,6 +54,7 @@ app.*.map.json
*.g.*
*.freezed.dart
/lib/api
+/lib/generated
# openapi.json
# local environment
diff --git a/README.md b/README.md
index df3c260a..fb8ca1ef 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,11 @@
# Kover
+[](https://github.com/rodonisi/kover/actions/workflows/ci.yaml)
+[](https://github.com/rodonisi/kover/actions/workflows/build-and-deploy.yml)
+
+
+
An unofficial cross-platform [Kavita](https://www.kavitareader.com/) client.
@@ -114,3 +119,17 @@ To connect Kover to a Kavita instance:
+
+## Thanks
+
+
+
+
+
+
+
+
+
+
+- [Sentry](https://sentry.io/) for providing an open source license for their error reporting software.
+- [Weblate](https://weblate.org/) for providing free hosting to support translations.
diff --git a/docs/assets/sentry.svg b/docs/assets/sentry.svg
new file mode 100644
index 00000000..ff30f3f2
--- /dev/null
+++ b/docs/assets/sentry.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/l10n.yaml b/l10n.yaml
new file mode 100644
index 00000000..2825690d
--- /dev/null
+++ b/l10n.yaml
@@ -0,0 +1,4 @@
+arb-dir: lib/l10n
+output-dir: lib/generated/l10n
+template-arb-file: en.arb
+nullable-getter: false
diff --git a/lib/l10n/en.arb b/lib/l10n/en.arb
new file mode 100644
index 00000000..1e667b54
--- /dev/null
+++ b/lib/l10n/en.arb
@@ -0,0 +1,650 @@
+{
+ "@@locale": "en",
+ "onDeck": "On Deck",
+ "@onDeck": {
+ "description": "Label for the home page on deck section"
+ },
+ "recentlyUpdated": "Recently Updated",
+ "@recentlyUpdated": {
+ "description": "Label for the home page recently updated section"
+ },
+ "recentlyAdded": "Recently Added",
+ "@recentlyAdded": {
+ "description": "Label for the home page recently added section"
+ },
+ "home": "Home",
+ "@home": {
+ "description": "Label for the home page"
+ },
+ "wantToRead": "Want To Read",
+ "@wantToRead": {
+ "description": "Label for the want to read shelf"
+ },
+ "menu": "Menu",
+ "@menu": {
+ "description": "Label for the menu button"
+ },
+ "allSeries": "All Series",
+ "@allSeries": {
+ "description": "Label for the all series menu entry"
+ },
+ "collections": "Collections",
+ "@collections": {
+ "description": "Label for the collections menu entry"
+ },
+ "readingLists": "Reading Lists",
+ "@readingLists": {
+ "description": "Label for the reading lists menu entry"
+ },
+ "libraries": "Libraries",
+ "@libraries": {
+ "description": "Label for the libraries menu entry"
+ },
+ "settings": "Settings",
+ "@settings": {
+ "description": "Label for the settings menu entry"
+ },
+ "more": "More",
+ "@more": {
+ "description": "Label for the more section"
+ },
+ "downloadQueue": "Download Queue",
+ "@downloadQueue": {
+ "description": "Label for the download queue menu entry"
+ },
+ "general": "General",
+ "@general": {
+ "description": "Label for the general settings section"
+ },
+ "themeMode": "Theme Mode",
+ "@themeMode": {
+ "description": "Label for the theme mode setting"
+ },
+ "system": "System",
+ "@system": {
+ "description": "Label for the system theme mode option"
+ },
+ "light": "Light",
+ "@light": {
+ "description": "Label for the light theme mode option"
+ },
+ "dark": "Dark",
+ "@dark": {
+ "description": "Label for the dark theme mode option"
+ },
+ "outlinedTheme": "Outlined Theme",
+ "@outlinedTheme": {
+ "description": "Label for the outlined theme setting"
+ },
+ "sendDiagnostics": "Send anonymous crash reports and diagnostics",
+ "@sendDiagnostics": {
+ "description": "Label for the send diagnostics setting"
+ },
+ "sendDiagnosticsDescription": "Help improve the app by sending anonymous error and performance statistics. The data does not contain any personal information and is uniquely used to improve the app.",
+ "@sendDiagnosticsDescription": {
+ "description": "Description for the send diagnostics setting"
+ },
+ "version": "Version: {version} ({buildNumber})",
+ "@version": {
+ "description": "Label for the app version, with placeholders for version and build number",
+ "placeholders": {
+ "version": {
+ "description": "The version string of the application",
+ "type": "String",
+ "example": "1.0.0"
+ },
+ "buildNumber": {
+ "description": "The build number of the application",
+ "type": "String",
+ "example": "123"
+ }
+ }
+ },
+ "github": "GitHub",
+ "@github": {
+ "description": "Label for the GitHub link"
+ },
+ "madeWithLove": "Made with ❤️",
+ "@madeWithLove": {
+ "description": "Label for the made with love message"
+ },
+ "dataManagement": "Data Management",
+ "@dataManagement": {
+ "description": "Label for the data management section"
+ },
+ "downloadAllCovers": "Download All Covers",
+ "@downloadAllCovers": {
+ "description": "Label for the download all covers option"
+ },
+ "downloadAllCoversDescription": "If disabled, covers will only be downloaded together with chapters. Covers will still be fetched from the server on demand when not downloaded and a connection is available.",
+ "@downloadAllCoversDescription": {
+ "description": "Description for the download all covers option"
+ },
+ "maxConcurrentDownloads": "Max Concurrent Downloads",
+ "@maxConcurrentDownloads": {
+ "description": "Label for the max concurrent downloads setting"
+ },
+ "reclaimSpace": "Reclaim Space",
+ "@reclaimSpace": {
+ "description": "Label for the reclaim space button"
+ },
+ "clearDownloads": "Clear Downloads",
+ "@clearDownloads": {
+ "description": "Label for the clear downloads button"
+ },
+ "clearCovers": "Clear Covers",
+ "@clearCovers": {
+ "description": "Label for the clear covers button"
+ },
+ "clearDatabase": "Clear Database",
+ "@clearDatabase": {
+ "description": "Label for the clear database button"
+ },
+ "clearDatabaseDialogTitle": "Are you sure?",
+ "@clearDatabaseDialogTitle": {
+ "description": "Title for the clear database confirmation dialog"
+ },
+ "clearDatabaseDialogContent": "This will clear the entire local database, including any unsynced progress and downloaded data. This action cannot be undone.",
+ "@clearDatabaseDialogContent": {
+ "description": "Content for the clear database confirmation dialog"
+ },
+ "cancel": "Cancel",
+ "@cancel": {
+ "description": "Label for the cancel button in dialogs"
+ },
+ "databaseBusy": "Database busy…",
+ "@databaseBusy": {
+ "description": "Message shown when the database is busy with an operation"
+ },
+ "databaseSize": "Database Size",
+ "@databaseSize": {
+ "description": "Label for the database size information"
+ },
+ "credentials": "Credentials",
+ "@credentials": {
+ "description": "Label for the credentials section in settings"
+ },
+ "baseUrl": "Base URL",
+ "@baseUrl": {
+ "description": "Label for the base URL input field"
+ },
+ "apiKey": "API Key",
+ "@apiKey": {
+ "description": "Label for the API key input field"
+ },
+ "save": "Save",
+ "@save": {
+ "description": "Label for the save credentials button"
+ },
+ "sortBy": "Sort By",
+ "@sortBy": {
+ "description": "Label for the sort by option in list"
+ },
+ "name": "Name",
+ "@name": {
+ "description": "Label for sorting by name option"
+ },
+ "dateAdded": "Date Added",
+ "@dateAdded": {
+ "description": "Label for sorting by date added option"
+ },
+ "lastModified": "Last Modified",
+ "@lastModified": {
+ "description": "Label for sorting by last modified option"
+ },
+ "sortDirection": "Sort Direction",
+ "@sortDirection": {
+ "description": "Label for the sort direction option in list"
+ },
+ "ascending": "Ascending",
+ "@ascending": {
+ "description": "Label for ascending sort direction option"
+ },
+ "descending": "Descending",
+ "@descending": {
+ "description": "Label for descending sort direction option"
+ },
+ "specials": "Specials",
+ "@specials": {
+ "description": "Label for the specials section in the series details page"
+ },
+ "storyline": "Storyline",
+ "@storyline": {
+ "description": "Label for the storyline section in the series details page"
+ },
+ "volumes": "Volumes",
+ "@volumes": {
+ "description": "Label for the volumes section in the series details page"
+ },
+ "chapters": "Chapters",
+ "@chapters": {
+ "description": "Label for the chapters section in the series details page"
+ },
+ "genres": "Genres",
+ "@genres": {
+ "description": "Label for the genres chips in the series details page"
+ },
+ "writers": "Writers",
+ "@writers": {
+ "description": "Label for the writers metadata in the series details page"
+ },
+ "filter": "Filter",
+ "@filter": {
+ "description": "Label for the filter button in lists"
+ },
+ "hideRead": "Hide Read",
+ "@hideRead": {
+ "description": "Label for the hide read filter option in lists"
+ },
+ "goToChapter": "Go to Chapter",
+ "@goToChapter": {
+ "description": "Label for the go to chapter button in the reading list chapter entry context menu"
+ },
+ "goToSeries": "Go to Series",
+ "@goToSeries": {
+ "description": "Label for the go to series button in the reading list chapter entry context menu"
+ },
+ "markAsRead": "Mark as Read",
+ "@markAsRead": {
+ "description": "Label for the mark as read button in context menus"
+ },
+ "markAsUnread": "Mark as Unread",
+ "@markAsUnread": {
+ "description": "Label for the mark as unread button in context menus"
+ },
+ "items": "{count} {count, plural, =0{{count} items} =1{{count} item} other{{count} items}}",
+ "@items": {
+ "description": "Label for the number of items, with pluralization",
+ "placeholders": {
+ "count": {
+ "description": "The amount of items",
+ "type": "int",
+ "example": "2"
+ }
+ }
+ },
+ "unsupportedFormat": "Unsupported format: {format}",
+ "@unsupportedFormat": {
+ "description": "Message shown when trying to open a chapter in an unsupported format",
+ "placeholders": {
+ "format": {
+ "description": "The format enum name for the unsupported format",
+ "type": "String",
+ "example": "epub"
+ }
+ }
+ },
+ "back": "Back",
+ "@back": {
+ "description": "Label for back buttons"
+ },
+ "tableOfContents": "Table of Contents",
+ "@tableOfContents": {
+ "description": "Label for the table of contents drawer in the reader"
+ },
+ "notSignedIn": "Not Signed In",
+ "@notSignedIn": {
+ "description": "Heading text when user is not signed in"
+ },
+ "noCredentialsDescription": "No credentials configured. Please add your server URL and API key in Settings.",
+ "@noCredentialsDescription": {
+ "description": "Description text when no credentials are configured"
+ },
+ "openSettings": "Open Settings",
+ "@openSettings": {
+ "description": "Label for the button to open settings"
+ },
+ "connectionError": "Connection Error",
+ "@connectionError": {
+ "description": "Heading text when a connection error occurs"
+ },
+ "connectionErrorDescription": "Failed to fetch user. Please check your credentials or try again.",
+ "@connectionErrorDescription": {
+ "description": "Description text when a connection error occurs"
+ },
+ "retry": "Retry",
+ "@retry": {
+ "description": "Label for the retry button"
+ },
+ "sendDiagnosticsDialogTitle": "Send anonymous crash reports and diagnostics?",
+ "@sendDiagnosticsDialogTitle": {
+ "description": "Title for the monitoring opt-out dialog"
+ },
+ "sendDiagnosticsChangeable": "This can be changed in the settings at any time.",
+ "@sendDiagnosticsChangeable": {
+ "description": "Message shown in the monitoring opt-out dialog indicating the setting can be changed later"
+ },
+ "noThanks": "No, thanks",
+ "@noThanks": {
+ "description": "Label for the decline button in the monitoring opt-out dialog"
+ },
+ "imIn": "I'm in!",
+ "@imIn": {
+ "description": "Label for the accept button in the monitoring opt-out dialog"
+ },
+ "addToWantToRead": "Add to Want to Read",
+ "@addToWantToRead": {
+ "description": "Label for the add to want to read context menu item"
+ },
+ "removeFromWantToRead": "Remove from Want to Read",
+ "@removeFromWantToRead": {
+ "description": "Label for the remove from want to read context menu item"
+ },
+ "download": "Download",
+ "@download": {
+ "description": "Label for the download context menu item"
+ },
+ "removeDownload": "Remove Download",
+ "@removeDownload": {
+ "description": "Label for the remove download context menu item"
+ },
+ "refreshMetadata": "Refresh Metadata",
+ "@refreshMetadata": {
+ "description": "Label for the refresh metadata context menu item"
+ },
+ "refreshCovers": "Refresh Covers",
+ "@refreshCovers": {
+ "description": "Label for the refresh covers context menu item"
+ },
+ "noActiveSyncOperations": "No active sync operations",
+ "@noActiveSyncOperations": {
+ "description": "Message shown when there are no active sync operations"
+ },
+ "syncingAllSeries": "Syncing all series",
+ "@syncingAllSeries": {
+ "description": "Label for the syncing all series phase"
+ },
+ "syncingMetadata": "Syncing metadata",
+ "@syncingMetadata": {
+ "description": "Label for the syncing metadata phase"
+ },
+ "syncingRecentlyAdded": "Syncing recently added",
+ "@syncingRecentlyAdded": {
+ "description": "Label for the syncing recently added phase"
+ },
+ "syncingRecentlyUpdated": "Syncing recently updated",
+ "@syncingRecentlyUpdated": {
+ "description": "Label for the syncing recently updated phase"
+ },
+ "syncingLibraries": "Syncing libraries",
+ "@syncingLibraries": {
+ "description": "Label for the syncing libraries phase"
+ },
+ "syncingProgress": "Syncing progress",
+ "@syncingProgress": {
+ "description": "Label for the syncing progress phase"
+ },
+ "syncingCovers": "Syncing covers",
+ "@syncingCovers": {
+ "description": "Label for the syncing covers phase"
+ },
+ "syncingCollections": "Syncing collections",
+ "@syncingCollections": {
+ "description": "Label for the syncing collections phase"
+ },
+ "syncingReadingLists": "Syncing reading lists",
+ "@syncingReadingLists": {
+ "description": "Label for the syncing reading lists phase"
+ },
+ "refreshingMetadataForSeries": "Refreshing metadata for series {seriesId}",
+ "@refreshingMetadataForSeries": {
+ "description": "Label for the refreshing metadata phase with the series ID",
+ "placeholders": {
+ "seriesId": {
+ "description": "The ID of the series being refreshed",
+ "type": "int",
+ "example": "42"
+ }
+ }
+ },
+ "refreshingCoversForSeries": "Refreshing covers for series {seriesId}",
+ "@refreshingCoversForSeries": {
+ "description": "Label for the refreshing covers phase with the series ID",
+ "placeholders": {
+ "seriesId": {
+ "description": "The ID of the series which covers are being refreshed",
+ "type": "int",
+ "example": "42"
+ }
+ }
+ },
+ "refreshingServerSettings": "Refreshing server settings",
+ "@refreshingServerSettings": {
+ "description": "Label for the refreshing server settings phase"
+ },
+ "series": "Series",
+ "@series": {
+ "description": "Label for the series search section header"
+ },
+ "moreCount": "+{count} more",
+ "@moreCount": {
+ "description": "Label indicating there are more items beyond the displayed amount",
+ "placeholders": {
+ "count": {
+ "description": "The count of items beyond the displayed amount",
+ "type": "int",
+ "example": "42"
+ }
+ }
+ },
+ "wordCount": "{wordCount} words",
+ "@wordCount": {
+ "description": "Label for the word count of a book entry",
+ "placeholders": {
+ "wordCount": {
+ "description": "The amount of words",
+ "type": "int",
+ "format": "compact",
+ "example": "42000"
+ }
+ }
+ },
+ "hoursCount": "~{hours} hours",
+ "@hoursCount": {
+ "description": "Label for the estimated reading time in hours",
+ "placeholders": {
+ "hours": {
+ "description": "The estimated amount of hours",
+ "type": "double",
+ "format": "decimalPatternDigits",
+ "optionalParameters": {
+ "decimalDigits": 1
+ },
+ "example": "4.2"
+ }
+ }
+ },
+ "pagesCount": "{pages} pages",
+ "@pagesCount": {
+ "description": "Label for the page count of a an entry",
+ "placeholders": {
+ "pages": {
+ "description": "The amount of pages",
+ "type": "int",
+ "format": "compact",
+ "example": "42000"
+ }
+ }
+ },
+ "continueReading": "Continue Reading",
+ "@continueReading": {
+ "description": "Label for the continue reading button"
+ },
+ "summary": "Summary",
+ "@summary": {
+ "description": "Label for the summary section heading"
+ },
+ "showMore": "Show More",
+ "@showMore": {
+ "description": "Label for the show more button"
+ },
+ "showLess": "Show Less",
+ "@showLess": {
+ "description": "Label for the show less button"
+ },
+ "cancelAll": "Cancel All",
+ "@cancelAll": {
+ "description": "Label for the cancel all downloads button"
+ },
+ "noDownloadsInQueue": "No downloads in queue",
+ "@noDownloadsInQueue": {
+ "description": "Message shown when there are no downloads in the queue"
+ },
+ "readerSettings": "Reader Settings",
+ "@readerSettings": {
+ "description": "Label for the reader settings bottom sheet title"
+ },
+ "readingDirection": "Reading Direction",
+ "@readingDirection": {
+ "description": "Label for the reading direction setting"
+ },
+ "leftToRight": "Left to Right",
+ "@leftToRight": {
+ "description": "Label for the left to right reading direction option"
+ },
+ "rightToLeft": "Right to Left",
+ "@rightToLeft": {
+ "description": "Label for the right to left reading direction option"
+ },
+ "readerMode": "Reader Mode",
+ "@readerMode": {
+ "description": "Label for the reader mode setting"
+ },
+ "vertical": "Vertical",
+ "@vertical": {
+ "description": "Label for the vertical reader mode option"
+ },
+ "horizontal": "Horizontal",
+ "@horizontal": {
+ "description": "Label for the horizontal reader mode option"
+ },
+ "twoPage": "Two Page",
+ "@twoPage": {
+ "description": "Label for the two page reader mode option"
+ },
+ "fitDirection": "Fit Direction",
+ "@fitDirection": {
+ "description": "Label for the image fit direction setting"
+ },
+ "contain": "Contain",
+ "@contain": {
+ "description": "Label for the contain image fit option"
+ },
+ "width": "Width",
+ "@width": {
+ "description": "Label for the fit width image option"
+ },
+ "height": "Height",
+ "@height": {
+ "description": "Label for the fit height image option"
+ },
+ "margins": "Margins",
+ "@margins": {
+ "description": "Label for the margins setting"
+ },
+ "verticalGap": "Vertical Gap",
+ "@verticalGap": {
+ "description": "Label for the vertical gap setting"
+ },
+ "pageGap": "Page Gap",
+ "@pageGap": {
+ "description": "Label for the page gap setting"
+ },
+ "coverPage": "Cover Page",
+ "@coverPage": {
+ "description": "Label for the cover page setting"
+ },
+ "coverPageDescription": "Treat the first page as the cover, showing it as a single page",
+ "@coverPageDescription": {
+ "description": "Description for the cover page setting"
+ },
+ "ignoreSafeAreas": "Ignore Safe Areas",
+ "@ignoreSafeAreas": {
+ "description": "Label for the ignore safe areas setting"
+ },
+ "showProgressBar": "Show Progress Bar",
+ "@showProgressBar": {
+ "description": "Label for the show progress bar setting"
+ },
+ "setDefaults": "Set Defaults",
+ "@setDefaults": {
+ "description": "Label for the set defaults button"
+ },
+ "reset": "Reset",
+ "@reset": {
+ "description": "Label for the reset button"
+ },
+ "fontSize": "Font Size",
+ "@fontSize": {
+ "description": "Label for the font size setting"
+ },
+ "lineHeight": "Line Height",
+ "@lineHeight": {
+ "description": "Label for the line height setting"
+ },
+ "wordSpacing": "Word Spacing",
+ "@wordSpacing": {
+ "description": "Label for the word spacing setting"
+ },
+ "letterSpacing": "Letter Spacing",
+ "@letterSpacing": {
+ "description": "Label for the letter spacing setting"
+ },
+ "highlightResumeParagraph": "Highlight Resume Paragraph",
+ "@highlightResumeParagraph": {
+ "description": "Label for the highlight resume paragraph setting"
+ },
+ "dismiss": "Dismiss",
+ "@dismiss": {
+ "description": "Label for the dismiss button in chapter snackbar"
+ },
+ "go": "Go",
+ "@go": {
+ "description": "Label for the go button in chapter snackbar"
+ },
+ "previousChapter": "Previous: {chapterTitle}",
+ "@previousChapter": {
+ "description": "Label for the previous chapter snackbar with the chapter title",
+ "placeholders": {
+ "chapterTitle": {
+ "description": "The title of the previous chapter",
+ "type": "String",
+ "example": "Chapter 2"
+ }
+ }
+ },
+ "nextChapter": "Next: {chapterTitle}",
+ "@nextChapter": {
+ "description": "Label for the next chapter snackbar with the chapter title",
+ "placeholders": {
+ "chapterTitle": {
+ "description": "The title of the next chapter",
+ "type": "String",
+ "example": "Chapter 3"
+ }
+ }
+ },
+ "read": "Read",
+ "@read": {
+ "description": "Label for the read action button on cover cards"
+ },
+ "syncingTocs": "Syncing chapters TOCs",
+ "@syncingTocs": {
+ "description": "Label for the syncing chapters TOCs phase"
+ },
+ "refreshingChapterToc": "Refreshing chapter TOC for chapter {chapterId}",
+ "@refreshingChapterToc": {
+ "description": "Label for the syncing chapter TOC phase with the chapter ID",
+ "placeholders": {
+ "chapterId": {
+ "description": "The ID of the chapter which TOC is being refreshed",
+ "type": "int",
+ "example": "42"
+ }
+ }
+ },
+ "invalidCredentials": "Invalid Credentials",
+ "@invalidCredentials": {
+ "description": "User display error displayed when credentials are invalid"
+ }
+}
diff --git a/lib/l10n/it.arb b/lib/l10n/it.arb
new file mode 100644
index 00000000..25a6713f
--- /dev/null
+++ b/lib/l10n/it.arb
@@ -0,0 +1,554 @@
+{
+ "addToWantToRead": "Aggiungi da leggere",
+ "@addToWantToRead": {
+ "description": "Label for the add to want to read context menu item"
+ },
+ "allSeries": "Tutte le serie",
+ "@allSeries": {
+ "description": "Label for the all series menu entry"
+ },
+ "apiKey": "API key",
+ "@apiKey": {
+ "description": "Label for the API key input field"
+ },
+ "ascending": "Crescente",
+ "@ascending": {
+ "description": "Label for ascending sort direction option"
+ },
+ "back": "Indietro",
+ "@back": {
+ "description": "Label for back buttons"
+ },
+ "baseUrl": "URL di base",
+ "@baseUrl": {
+ "description": "Label for the base URL input field"
+ },
+ "cancel": "Annullare",
+ "@cancel": {
+ "description": "Label for the cancel button in dialogs"
+ },
+ "cancelAll": "Cancella tutto",
+ "@cancelAll": {
+ "description": "Label for the cancel all downloads button"
+ },
+ "chapters": "Capitoli",
+ "@chapters": {
+ "description": "Label for the chapters section in the series details page"
+ },
+ "clearCovers": "Rimuovi copertine",
+ "@clearCovers": {
+ "description": "Label for the clear covers button"
+ },
+ "clearDatabase": "Pulisci database",
+ "@clearDatabase": {
+ "description": "Label for the clear database button"
+ },
+ "clearDatabaseDialogContent": "Questa operazione cancellerà l'intero database locale, inclusi i progressi non sincronizzati e i dati scaricati. Questa azione non può essere annullata.",
+ "@clearDatabaseDialogContent": {
+ "description": "Content for the clear database confirmation dialog"
+ },
+ "clearDatabaseDialogTitle": "Sei sicuro/a?",
+ "@clearDatabaseDialogTitle": {
+ "description": "Title for the clear database confirmation dialog"
+ },
+ "clearDownloads": "Rimuovi downloads",
+ "@clearDownloads": {
+ "description": "Label for the clear downloads button"
+ },
+ "collections": "Collezioni",
+ "@collections": {
+ "description": "Label for the collections menu entry"
+ },
+ "connectionError": "Errore di connessione",
+ "@connectionError": {
+ "description": "Heading text when a connection error occurs"
+ },
+ "connectionErrorDescription": "Impossibile recuperare l'utente. Verifica le tue credenziali o riprova.",
+ "@connectionErrorDescription": {
+ "description": "Description text when a connection error occurs"
+ },
+ "contain": "Contieni",
+ "@contain": {
+ "description": "Label for the contain image fit option"
+ },
+ "continueReading": "Continua a leggere",
+ "@continueReading": {
+ "description": "Label for the continue reading button"
+ },
+ "coverPage": "Pagina di copertina",
+ "@coverPage": {
+ "description": "Label for the cover page setting"
+ },
+ "coverPageDescription": "Trattare la prima pagina come copertina, visualizzandola come una pagina singola",
+ "@coverPageDescription": {
+ "description": "Description for the cover page setting"
+ },
+ "credentials": "Credenziali",
+ "@credentials": {
+ "description": "Label for the credentials section in settings"
+ },
+ "dark": "Scuro",
+ "@dark": {
+ "description": "Label for the dark theme mode option"
+ },
+ "dataManagement": "Gestione dei Dati",
+ "@dataManagement": {
+ "description": "Label for the data management section"
+ },
+ "databaseBusy": "Database occupato…",
+ "@databaseBusy": {
+ "description": "Message shown when the database is busy with an operation"
+ },
+ "databaseSize": "Dimensione database",
+ "@databaseSize": {
+ "description": "Label for the database size information"
+ },
+ "dateAdded": "Data di aggiunta",
+ "@dateAdded": {
+ "description": "Label for sorting by date added option"
+ },
+ "descending": "Decrescente",
+ "@descending": {
+ "description": "Label for descending sort direction option"
+ },
+ "dismiss": "Chiudi",
+ "@dismiss": {
+ "description": "Label for the dismiss button in chapter snackbar"
+ },
+ "download": "Scarica",
+ "@download": {
+ "description": "Label for the download context menu item"
+ },
+ "downloadAllCovers": "Scarica tutte le copertine",
+ "@downloadAllCovers": {
+ "description": "Label for the download all covers option"
+ },
+ "downloadAllCoversDescription": "Se disattivata, le copertine verranno scaricate solo insieme ai capitoli. Le copertine non scaricate verranno ancora recuperate dal server su richiesta, se è disponibile una connessione.",
+ "@downloadAllCoversDescription": {
+ "description": "Description for the download all covers option"
+ },
+ "downloadQueue": "Coda download",
+ "@downloadQueue": {
+ "description": "Label for the download queue menu entry"
+ },
+ "filter": "Filtro",
+ "@filter": {
+ "description": "Label for the filter button in lists"
+ },
+ "fitDirection": "Direzione adattamento",
+ "@fitDirection": {
+ "description": "Label for the image fit direction setting"
+ },
+ "fontSize": "Dimensione carattere",
+ "@fontSize": {
+ "description": "Label for the font size setting"
+ },
+ "general": "Generali",
+ "@general": {
+ "description": "Label for the general settings section"
+ },
+ "genres": "Generi",
+ "@genres": {
+ "description": "Label for the genres chips in the series details page"
+ },
+ "github": "GitHub",
+ "@github": {
+ "description": "Label for the GitHub link"
+ },
+ "go": "Vai",
+ "@go": {
+ "description": "Label for the go button in chapter snackbar"
+ },
+ "goToChapter": "Vai al capitolo",
+ "@goToChapter": {
+ "description": "Label for the go to chapter button in the reading list chapter entry context menu"
+ },
+ "goToSeries": "Vai alla serie",
+ "@goToSeries": {
+ "description": "Label for the go to series button in the reading list chapter entry context menu"
+ },
+ "height": "Altezza",
+ "@height": {
+ "description": "Label for the fit height image option"
+ },
+ "hideRead": "Nascondi letti",
+ "@hideRead": {
+ "description": "Label for the hide read filter option in lists"
+ },
+ "highlightResumeParagraph": "Evidenzia paragrafo di ripresa",
+ "@highlightResumeParagraph": {
+ "description": "Label for the highlight resume paragraph setting"
+ },
+ "home": "Panoramica",
+ "@home": {
+ "description": "Label for the home page"
+ },
+ "horizontal": "Orizzontale",
+ "@horizontal": {
+ "description": "Label for the horizontal reader mode option"
+ },
+ "hoursCount": "~{hours} ore",
+ "@hoursCount": {
+ "description": "Label for the estimated reading time in hours"
+ },
+ "ignoreSafeAreas": "Ignora aree sicure",
+ "@ignoreSafeAreas": {
+ "description": "Label for the ignore safe areas setting"
+ },
+ "imIn": "Ci sto!",
+ "@imIn": {
+ "description": "Label for the accept button in the monitoring opt-out dialog"
+ },
+ "items": "{count} {count, plural, =0{{count} elementi} =1{{count} elemento} other{{count} elementi}}",
+ "@items": {
+ "description": "Label for the number of items, with pluralization"
+ },
+ "lastModified": "Ultima modifica",
+ "@lastModified": {
+ "description": "Label for sorting by last modified option"
+ },
+ "leftToRight": "Sinistra a destra",
+ "@leftToRight": {
+ "description": "Label for the left to right reading direction option"
+ },
+ "letterSpacing": "Spaziatura lettere",
+ "@letterSpacing": {
+ "description": "Label for the letter spacing setting"
+ },
+ "libraries": "Librerie",
+ "@libraries": {
+ "description": "Label for the libraries menu entry"
+ },
+ "light": "Chiaro",
+ "@light": {
+ "description": "Label for the light theme mode option"
+ },
+ "lineHeight": "Altezza della linea",
+ "@lineHeight": {
+ "description": "Label for the line height setting"
+ },
+ "madeWithLove": "Realizzato con ❤️",
+ "@madeWithLove": {
+ "description": "Label for the made with love message"
+ },
+ "margins": "Margini",
+ "@margins": {
+ "description": "Label for the margins setting"
+ },
+ "markAsRead": "Segna come letto",
+ "@markAsRead": {
+ "description": "Label for the mark as read button in context menus"
+ },
+ "markAsUnread": "Segna come non letto",
+ "@markAsUnread": {
+ "description": "Label for the mark as unread button in context menus"
+ },
+ "maxConcurrentDownloads": "Download simultanei",
+ "@maxConcurrentDownloads": {
+ "description": "Label for the max concurrent downloads setting"
+ },
+ "menu": "Menu",
+ "@menu": {
+ "description": "Label for the menu button"
+ },
+ "more": "Altro",
+ "@more": {
+ "description": "Label for the more section"
+ },
+ "moreCount": "+{count} in più",
+ "@moreCount": {
+ "description": "Label indicating there are more items beyond the displayed count"
+ },
+ "name": "Nome",
+ "@name": {
+ "description": "Label for sorting by name option"
+ },
+ "nextChapter": "Prossimo: {chapterTitle}",
+ "@nextChapter": {
+ "description": "Label for the next chapter snackbar with the chapter title"
+ },
+ "noActiveSyncOperations": "Nessuna operazione di sincronizzazione attiva",
+ "@noActiveSyncOperations": {
+ "description": "Message shown when there are no active sync operations"
+ },
+ "noCredentialsDescription": "Nessuna configurazione delle credenziali. Si prega di aggiungere l'URL del server e la chiave API in Impostazioni.",
+ "@noCredentialsDescription": {
+ "description": "Description text when no credentials are configured"
+ },
+ "noDownloadsInQueue": "Nessun download in coda",
+ "@noDownloadsInQueue": {
+ "description": "Message shown when there are no downloads in the queue"
+ },
+ "noThanks": "No, grazie",
+ "@noThanks": {
+ "description": "Label for the decline button in the monitoring opt-out dialog"
+ },
+ "notSignedIn": "Non connesso",
+ "@notSignedIn": {
+ "description": "Heading text when user is not signed in"
+ },
+ "onDeck": "In primo piano",
+ "@onDeck": {
+ "description": "Label for the home page on deck section"
+ },
+ "openSettings": "Apri impostazioni",
+ "@openSettings": {
+ "description": "Label for the button to open settings"
+ },
+ "outlinedTheme": "Tema delineato",
+ "@outlinedTheme": {
+ "description": "Label for the outlined theme setting"
+ },
+ "pageGap": "Spazio tra pagine",
+ "@pageGap": {
+ "description": "Label for the page gap setting"
+ },
+ "pagesCount": "{pages} pagine",
+ "@pagesCount": {
+ "description": "Label for the page count of a chapter"
+ },
+ "previousChapter": "Precedente: {chapterTitle}",
+ "@previousChapter": {
+ "description": "Label for the previous chapter snackbar with the chapter title"
+ },
+ "read": "Leggi",
+ "@read": {
+ "description": "Label for the read action button on cover cards"
+ },
+ "readerMode": "Modalità lettore",
+ "@readerMode": {
+ "description": "Label for the reader mode setting"
+ },
+ "readerSettings": "Impostazioni lettore",
+ "@readerSettings": {
+ "description": "Label for the reader settings bottom sheet title"
+ },
+ "readingDirection": "Direzione di lettura",
+ "@readingDirection": {
+ "description": "Label for the reading direction setting"
+ },
+ "readingLists": "Liste di lettura",
+ "@readingLists": {
+ "description": "Label for the reading lists menu entry"
+ },
+ "recentlyAdded": "Aggiunto recentemente",
+ "@recentlyAdded": {
+ "description": "Label for the home page recently added section"
+ },
+ "recentlyUpdated": "Aggiornato recentemente",
+ "@recentlyUpdated": {
+ "description": "Label for the home page recently updated section"
+ },
+ "reclaimSpace": "Recupera spazio",
+ "@reclaimSpace": {
+ "description": "Label for the reclaim space button"
+ },
+ "refreshCovers": "Aggiorna copertine",
+ "@refreshCovers": {
+ "description": "Label for the refresh covers context menu item"
+ },
+ "refreshMetadata": "Aggiorna metadati",
+ "@refreshMetadata": {
+ "description": "Label for the refresh metadata context menu item"
+ },
+ "refreshingCoversForSeries": "Aggiornamento copertine per serie {seriesId}",
+ "@refreshingCoversForSeries": {
+ "description": "Label for the refreshing covers phase with the series ID"
+ },
+ "refreshingMetadataForSeries": "Aggiornamento metadati per serie {seriesId}",
+ "@refreshingMetadataForSeries": {
+ "description": "Label for the refreshing metadata phase with the series ID"
+ },
+ "refreshingServerSettings": "Aggiornamento impostazioni del server",
+ "@refreshingServerSettings": {
+ "description": "Label for the refreshing server settings phase"
+ },
+ "removeDownload": "Rimuovi download",
+ "@removeDownload": {
+ "description": "Label for the remove download context menu item"
+ },
+ "removeFromWantToRead": "Rimuovi da leggere",
+ "@removeFromWantToRead": {
+ "description": "Label for the remove from want to read context menu item"
+ },
+ "reset": "Resetta",
+ "@reset": {
+ "description": "Label for the reset button"
+ },
+ "retry": "Riprova",
+ "@retry": {
+ "description": "Label for the retry button"
+ },
+ "rightToLeft": "Destra a sinistra",
+ "@rightToLeft": {
+ "description": "Label for the right to left reading direction option"
+ },
+ "save": "Salva",
+ "@save": {
+ "description": "Label for the save credentials button"
+ },
+ "sendDiagnostics": "Invia statistiche anonime di arresto e diagnostica",
+ "@sendDiagnostics": {
+ "description": "Label for the send diagnostics setting"
+ },
+ "sendDiagnosticsChangeable": "Questo può essere modificato nelle impostazioni in qualsiasi momento.",
+ "@sendDiagnosticsChangeable": {
+ "description": "Message shown in the monitoring opt-out dialog indicating the setting can be changed later"
+ },
+ "sendDiagnosticsDescription": "Aiuta a migliorare l'app inviando statistiche anonime di errore e prestazioni. I dati non contengono alcuna informazione personale e sono utilizzati in unicamente per migliorare l'app.",
+ "@sendDiagnosticsDescription": {
+ "description": "Description for the send diagnostics setting"
+ },
+ "sendDiagnosticsDialogTitle": "Invia statistiche anonime di arresto e diagnostica?",
+ "@sendDiagnosticsDialogTitle": {
+ "description": "Title for the monitoring opt-out dialog"
+ },
+ "series": "Serie",
+ "@series": {
+ "description": "Label for the series search section header"
+ },
+ "setDefaults": "Impostare i default",
+ "@setDefaults": {
+ "description": "Label for the set defaults button"
+ },
+ "settings": "Impostazioni",
+ "@settings": {
+ "description": "Label for the settings menu entry"
+ },
+ "showLess": "Mostra di meno",
+ "@showLess": {
+ "description": "Label for the show less button"
+ },
+ "showMore": "Mostra di più",
+ "@showMore": {
+ "description": "Label for the show more button"
+ },
+ "showProgressBar": "Mostra barra di progresso",
+ "@showProgressBar": {
+ "description": "Label for the show progress bar setting"
+ },
+ "sortBy": "Ordina per",
+ "@sortBy": {
+ "description": "Label for the sort by option in list"
+ },
+ "sortDirection": "Direzione ordine",
+ "@sortDirection": {
+ "description": "Label for the sort direction option in list"
+ },
+ "specials": "Speciali",
+ "@specials": {
+ "description": "Label for the specials section in the series details page"
+ },
+ "storyline": "Trama",
+ "@storyline": {
+ "description": "Label for the storyline section in the series details page"
+ },
+ "summary": "Sinossi",
+ "@summary": {
+ "description": "Label for the summary section heading"
+ },
+ "syncingAllSeries": "Sincronizzazione di tutte le serie",
+ "@syncingAllSeries": {
+ "description": "Label for the syncing all series phase"
+ },
+ "syncingCollections": "Sincronizzazione collezioni",
+ "@syncingCollections": {
+ "description": "Label for the syncing collections phase"
+ },
+ "syncingCovers": "Sincronizzazione copertine",
+ "@syncingCovers": {
+ "description": "Label for the syncing covers phase"
+ },
+ "syncingLibraries": "Sincronizzazione delle librerie",
+ "@syncingLibraries": {
+ "description": "Label for the syncing libraries phase"
+ },
+ "syncingMetadata": "Sincronizzazione dei metadati",
+ "@syncingMetadata": {
+ "description": "Label for the syncing metadata phase"
+ },
+ "syncingProgress": "Sincronizzazione dei progressi",
+ "@syncingProgress": {
+ "description": "Label for the syncing progress phase"
+ },
+ "syncingReadingLists": "Sincronizzazione liste di lettura",
+ "@syncingReadingLists": {
+ "description": "Label for the syncing reading lists phase"
+ },
+ "syncingRecentlyAdded": "Sincronizzazione recentemente aggiunti",
+ "@syncingRecentlyAdded": {
+ "description": "Label for the syncing recently added phase"
+ },
+ "syncingRecentlyUpdated": "Sincronizzazione recentemente aggiornati",
+ "@syncingRecentlyUpdated": {
+ "description": "Label for the syncing recently updated phase"
+ },
+ "system": "Sistema",
+ "@system": {
+ "description": "Label for the system theme mode option"
+ },
+ "tableOfContents": "Indice",
+ "@tableOfContents": {
+ "description": "Label for the table of contents drawer in the reader"
+ },
+ "themeMode": "Modalità tema",
+ "@themeMode": {
+ "description": "Label for the theme mode setting"
+ },
+ "twoPage": "Due pagine",
+ "@twoPage": {
+ "description": "Label for the two page reader mode option"
+ },
+ "unsupportedFormat": "Formato non supportato: {format}",
+ "@unsupportedFormat": {
+ "description": "Message shown when trying to open a chapter in an unsupported format"
+ },
+ "version": "Versione: {version} ({buildNumber})",
+ "@version": {
+ "description": "Label for the app version, with placeholders for version and build number"
+ },
+ "vertical": "Verticale",
+ "@vertical": {
+ "description": "Label for the vertical reader mode option"
+ },
+ "verticalGap": "Distanza verticale",
+ "@verticalGap": {
+ "description": "Label for the vertical gap setting"
+ },
+ "volumes": "Volumi",
+ "@volumes": {
+ "description": "Label for the volumes section in the series details page"
+ },
+ "wantToRead": "Da leggere",
+ "@wantToRead": {
+ "description": "Label for the want to read shelf"
+ },
+ "width": "Larghezza",
+ "@width": {
+ "description": "Label for the fit width image option"
+ },
+ "wordCount": "{wordCount} parole",
+ "@wordCount": {
+ "description": "Label for the word count of a chapter"
+ },
+ "wordSpacing": "Spaziatura delle parole",
+ "@wordSpacing": {
+ "description": "Label for the word spacing setting"
+ },
+ "writers": "Scrittori",
+ "@writers": {
+ "description": "Label for the writers metadata in the series details page"
+ },
+ "refreshingChapterToc": "Aggiornamento indice per capitolo {chapterId}",
+ "@refreshingChapterToc": {
+ "description": "Label for the syncing chapter TOC phase with the chapter ID"
+ },
+ "syncingTocs": "Sincronizzazione indici capitoli",
+ "@syncingTocs": {
+ "description": "Label for the syncing chapters TOCs phase"
+ },
+ "invalidCredentials": "Credenziali non valide",
+ "@invalidCredentials": {
+ "description": "User display error displayed when credentials are invalid"
+ }
+}
diff --git a/lib/main.dart b/lib/main.dart
index 519136ad..3794f84e 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/riverpod/providers/router.dart';
import 'package:kover/riverpod/providers/theme.dart';
import 'package:kover/riverpod/repository/sentry_repository.dart';
@@ -40,6 +41,8 @@ class App extends ConsumerWidget {
darkTheme: theme.darkTheme,
themeMode: theme.mode,
routerConfig: ref.watch(routerProvider),
+ localizationsDelegates: AppLocalizations.localizationsDelegates,
+ supportedLocales: AppLocalizations.supportedLocales,
),
loading: () => const SizedBox.shrink(),
),
diff --git a/lib/pages/collections_page/collections_page.dart b/lib/pages/collections_page/collections_page.dart
index 62673b67..fa8a0c16 100644
--- a/lib/pages/collections_page/collections_page.dart
+++ b/lib/pages/collections_page/collections_page.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/models/collection_model.dart';
import 'package:kover/models/enums/sort_direction.dart';
import 'package:kover/riverpod/managers/sync_manager.dart';
@@ -20,6 +21,7 @@ class CollectionsPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final sortDirection = useState(SortDirection.ascending);
final controller = useTextEditingController();
final collections = ref.watch(collectionsProvider);
@@ -35,7 +37,7 @@ class CollectionsPage extends HookConsumerWidget {
keyboardDismissBehavior: .onDrag,
slivers: [
SliverAppBar.large(
- title: const Text('Collections'),
+ title: Text(l.collections),
actionsPadding: const EdgeInsets.symmetric(
horizontal: LayoutConstants.smallPadding,
),
@@ -46,7 +48,7 @@ class CollectionsPage extends HookConsumerWidget {
? KoverIcons.ascending
: KoverIcons.descending,
),
- menu: _menu(sortDirection),
+ menu: _menu(sortDirection: sortDirection, context: context),
),
],
),
@@ -114,14 +116,16 @@ class CollectionsPage extends HookConsumerWidget {
return sorted;
}
- ContextMenu _menu(
- ValueNotifier sortDirection,
- ) {
+ ContextMenu _menu({
+ required ValueNotifier sortDirection,
+ required BuildContext context,
+ }) {
+ final l = AppLocalizations.of(context);
return ContextMenu(
entries: [
- const MenuHeader(text: 'Direction'),
+ MenuHeader(text: l.sortDirection),
MenuItem(
- label: const Text('Ascending'),
+ label: Text(l.ascending),
icon: _getItemIcon(
sortDirection.value == .ascending,
),
@@ -130,7 +134,7 @@ class CollectionsPage extends HookConsumerWidget {
},
),
MenuItem(
- label: const Text('Descending'),
+ label: Text(l.descending),
icon: _getItemIcon(
sortDirection.value == .descending,
),
diff --git a/lib/pages/download_queue/download_queue_page.dart b/lib/pages/download_queue/download_queue_page.dart
index 8f587c43..d8c5091c 100644
--- a/lib/pages/download_queue/download_queue_page.dart
+++ b/lib/pages/download_queue/download_queue_page.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/riverpod/managers/download_manager.dart';
import 'package:kover/riverpod/providers/chapter.dart';
import 'package:kover/riverpod/providers/download.dart';
@@ -16,6 +17,7 @@ class DownloadQueuePage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final hasDls = ref.watch(
downloadManagerProvider.select(
(state) => state.value?.downloadQueue.isNotEmpty ?? false,
@@ -27,7 +29,7 @@ class DownloadQueuePage extends ConsumerWidget {
physics: hasDls ? null : const NeverScrollableScrollPhysics(),
slivers: [
SliverAppBar.large(
- title: const Text('Download Queue'),
+ title: Text(l.downloadQueue),
actions: [
if (hasDls) const CancellAllAction(),
],
@@ -52,11 +54,12 @@ class CancellAllAction extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
return TextButton(
onPressed: () async {
await ref.read(downloadManagerProvider.notifier).cancelAll();
},
- child: const Text('Cancel All'),
+ child: Text(l.cancelAll),
);
}
}
@@ -70,9 +73,10 @@ class DownloadQueueList extends ConsumerWidget {
return AsyncSliver(
asyncValue: queued,
data: (data) {
+ final l = AppLocalizations.of(context);
if (data.downloadQueue.isEmpty) {
- return const SliverFillRemaining(
- child: Center(child: Text('No downloads in queue')),
+ return SliverFillRemaining(
+ child: Center(child: Text(l.noDownloadsInQueue)),
);
}
final queued = data.downloadQueue.toList();
diff --git a/lib/pages/home/collapsible_section.dart b/lib/pages/home/collapsible_section.dart
index 04ed70bf..95522c80 100644
--- a/lib/pages/home/collapsible_section.dart
+++ b/lib/pages/home/collapsible_section.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/models/series_model.dart';
import 'package:kover/utils/layout_constants.dart';
import 'package:kover/widgets/lists/series_sliver_grid.dart';
@@ -17,6 +18,7 @@ class CollapsibleSection extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final showAll = useState(false);
final total = series.length;
@@ -42,7 +44,7 @@ class CollapsibleSection extends HookConsumerWidget {
onPressed: () {
showAll.value = !showAll.value;
},
- child: Text(showAll.value ? 'Show Less' : 'Show All'),
+ child: Text(showAll.value ? l.showLess : l.showMore),
),
],
),
diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart
index 0ad31105..4f29ab91 100644
--- a/lib/pages/home/home_page.dart
+++ b/lib/pages/home/home_page.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/pages/home/collapsible_section.dart';
import 'package:kover/riverpod/managers/sync_manager.dart';
import 'package:kover/riverpod/providers/series.dart';
@@ -53,11 +54,12 @@ class OnDeck extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final onDeck = ref.watch(onDeckProvider);
return AsyncSliver(
asyncValue: onDeck,
- data: (data) => CollapsibleSection(title: 'On Deck', series: data),
+ data: (data) => CollapsibleSection(title: l.onDeck, series: data),
);
}
}
@@ -67,12 +69,13 @@ class RecentlyUpdated extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final series = ref.watch(recentlyUpdatedProvider);
return AsyncSliver(
asyncValue: series,
data: (data) =>
- CollapsibleSection(title: 'Recently Updated', series: data),
+ CollapsibleSection(title: l.recentlyUpdated, series: data),
);
}
}
@@ -82,11 +85,12 @@ class RecentlyAdded extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final series = ref.watch(recentlyAddedProvider);
return AsyncSliver(
asyncValue: series,
- data: (data) => CollapsibleSection(title: 'Recently Added', series: data),
+ data: (data) => CollapsibleSection(title: l.recentlyAdded, series: data),
);
}
}
diff --git a/lib/pages/menu_page/menu_page.dart b/lib/pages/menu_page/menu_page.dart
index 4f7102fd..dc87ab05 100644
--- a/lib/pages/menu_page/menu_page.dart
+++ b/lib/pages/menu_page/menu_page.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/pages/menu_page/app_list_tile.dart';
import 'package:kover/pages/menu_page/sliver_libraries.dart';
import 'package:kover/pages/menu_page/sliver_section.dart';
@@ -19,6 +20,7 @@ class MenuPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(syncManagerProvider.notifier).syncLibraries();
});
@@ -48,7 +50,7 @@ class MenuPage extends ConsumerWidget {
),
sliver: SliverToBoxAdapter(
child: AppListTile(
- title: 'All Series',
+ title: l.allSeries,
icon: const Icon(LucideIcons.list),
onTap: () => const AllSeriesRoute().push(context),
),
@@ -61,7 +63,7 @@ class MenuPage extends ConsumerWidget {
),
sliver: SliverToBoxAdapter(
child: AppListTile(
- title: 'Collections',
+ title: l.collections,
icon: const Icon(KoverIcons.collection),
onTap: () => const CollectionsRoute().push(context),
),
@@ -74,16 +76,16 @@ class MenuPage extends ConsumerWidget {
),
sliver: SliverToBoxAdapter(
child: AppListTile(
- title: 'Reading Lists',
+ title: l.readingLists,
icon: const Icon(KoverIcons.readingList),
onTap: () => const ReadingListsRoute().push(context),
),
),
),
- const SliverSection(title: 'Libraries'),
+ SliverSection(title: l.libraries),
const SliverLibraries(),
],
- const SliverSection(title: 'More'),
+ SliverSection(title: l.downloadQueue),
SliverPadding(
padding: const EdgeInsetsGeometry.symmetric(
vertical: LayoutConstants.smallerPadding,
@@ -91,7 +93,7 @@ class MenuPage extends ConsumerWidget {
),
sliver: SliverToBoxAdapter(
child: AppListTile(
- title: 'Download Queue',
+ title: l.downloadQueue,
icon: isDownloading
? const Icon(LucideIcons.refreshCw)
.animate(
@@ -110,7 +112,7 @@ class MenuPage extends ConsumerWidget {
),
sliver: SliverToBoxAdapter(
child: AppListTile(
- title: 'Settings',
+ title: l.settings,
icon: const Icon(LucideIcons.settings),
onTap: () => const SettingsRoute().push(context),
),
diff --git a/lib/pages/reader/epub_reader/epub_reader_controls.dart b/lib/pages/reader/epub_reader/epub_reader_controls.dart
index a6312980..9871f131 100644
--- a/lib/pages/reader/epub_reader/epub_reader_controls.dart
+++ b/lib/pages/reader/epub_reader/epub_reader_controls.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/models/read_direction.dart';
import 'package:kover/riverpod/providers/settings/epub_reader_settings.dart';
import 'package:kover/utils/constants/kover_icons.dart';
@@ -16,6 +17,7 @@ class EpubReaderSettingsBottomSheet extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final provider = epubReaderSettingsProvider(seriesId: seriesId);
return Async(
@@ -38,24 +40,24 @@ class EpubReaderSettingsBottomSheet extends ConsumerWidget {
spacing: LayoutConstants.largePadding,
children: [
Text(
- 'Reader Settings',
+ l.readerSettings,
style: Theme.of(context).textTheme.titleLarge,
),
ChoiceOption(
- title: 'Reading Direction',
+ title: l.readingDirection,
icon: settings.readDirection == .leftToRight
? LucideIcons.chevronsRight
: LucideIcons.chevronsLeft,
value: settings.readDirection,
- options: const [
+ options: [
ChoiceOptionEntry(
value: ReadDirection.leftToRight,
- label: 'Left To Right',
+ label: l.leftToRight,
icon: LucideIcons.chevronsRight,
),
ChoiceOptionEntry(
value: ReadDirection.rightToLeft,
- label: 'Right To Left',
+ label: l.rightToLeft,
icon: LucideIcons.chevronsLeft,
),
],
@@ -68,7 +70,7 @@ class EpubReaderSettingsBottomSheet extends ConsumerWidget {
},
),
NumericOption(
- title: 'Font Size',
+ title: l.fontSize,
icon: LucideIcons.aLargeSmallDir,
value: settings.fontSize,
min: EpubReaderSettingsLimits.fontSizeMin,
@@ -80,7 +82,7 @@ class EpubReaderSettingsBottomSheet extends ConsumerWidget {
.setFontSize(newValue),
),
NumericOption(
- title: 'Margins',
+ title: l.margins,
icon: LucideIcons.panelLeftDashed,
value: settings.marginSize,
min: EpubReaderSettingsLimits.marginSizeMin,
@@ -93,7 +95,7 @@ class EpubReaderSettingsBottomSheet extends ConsumerWidget {
),
NumericOption(
- title: 'Line Height',
+ title: l.lineHeight,
icon: LucideIcons.listChevronsUpDown,
value: settings.lineHeight,
min: EpubReaderSettingsLimits.lineHeightMin,
@@ -105,7 +107,7 @@ class EpubReaderSettingsBottomSheet extends ConsumerWidget {
),
NumericOption(
value: settings.wordSpacing,
- title: 'Word Spacing',
+ title: l.wordSpacing,
min: EpubReaderSettingsLimits.wordSpacingMin,
max: EpubReaderSettingsLimits.wordSpacingMax,
step: EpubReaderSettingsLimits.wordSpacingStep,
@@ -115,7 +117,7 @@ class EpubReaderSettingsBottomSheet extends ConsumerWidget {
icon: LucideIcons.listMinus,
),
NumericOption(
- title: 'Letter Spacing',
+ title: l.letterSpacing,
icon: LucideIcons.wholeWord,
value: settings.letterSpacing,
min: EpubReaderSettingsLimits.letterSpacingMin,
@@ -127,7 +129,7 @@ class EpubReaderSettingsBottomSheet extends ConsumerWidget {
),
BooleanOption(
icon: LucideIcons.highlighter,
- title: 'Highlight Resume Paragraph',
+ title: l.highlightResumeParagraph,
value: settings.highlightResumePoint,
onChanged: (value) async {
await ref
@@ -137,7 +139,7 @@ class EpubReaderSettingsBottomSheet extends ConsumerWidget {
),
BooleanOption(
icon: KoverIcons.progressBar,
- title: 'Show Progress Bar',
+ title: l.showProgressBar,
value: settings.showProgressBar,
onChanged: (value) async {
await ref
@@ -168,7 +170,7 @@ class EpubReaderSettingsBottomSheet extends ConsumerWidget {
onPressed: () async =>
await ref.read(provider.notifier).setDefault(),
icon: const Icon(LucideIcons.save),
- label: const Text('Set Defaults'),
+ label: Text(l.setDefaults),
),
),
Expanded(
@@ -176,7 +178,7 @@ class EpubReaderSettingsBottomSheet extends ConsumerWidget {
onPressed: () async =>
await ref.read(provider.notifier).reset(),
icon: const Icon(LucideIcons.rotateCcw),
- label: const Text('Reset'),
+ label: Text(l.reset),
),
),
],
diff --git a/lib/pages/reader/epub_reader/epub_toc_drawer.dart b/lib/pages/reader/epub_reader/epub_toc_drawer.dart
index 10228744..3c127273 100644
--- a/lib/pages/reader/epub_reader/epub_toc_drawer.dart
+++ b/lib/pages/reader/epub_reader/epub_toc_drawer.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/models/book_chapter_model.dart';
import 'package:kover/riverpod/managers/sync_manager.dart';
import 'package:kover/riverpod/providers/book.dart';
@@ -19,6 +20,7 @@ class EpubTocDrawer extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final selectedKey = useState(null);
final hasScrolled = useState(false);
@@ -73,7 +75,7 @@ class EpubTocDrawer extends HookConsumerWidget {
horizontal: LayoutConstants.mediumPadding,
),
child: Text(
- 'Table of Contents',
+ l.tableOfContents,
style: Theme.of(context).textTheme.headlineMedium,
),
),
diff --git a/lib/pages/reader/image_reader/image_reader_controls.dart b/lib/pages/reader/image_reader/image_reader_controls.dart
index 857d9532..6d061122 100644
--- a/lib/pages/reader/image_reader/image_reader_controls.dart
+++ b/lib/pages/reader/image_reader/image_reader_controls.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/models/read_direction.dart';
import 'package:kover/riverpod/providers/breakpoints.dart';
import 'package:kover/riverpod/providers/settings/image_reader_settings.dart';
@@ -18,8 +19,10 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final provider = imageReaderSettingsProvider(seriesId: seriesId);
final breakpoint = ref.watch(breakpointsProvider);
+
return Async(
asyncValue: ref.watch(provider),
data: (settings) {
@@ -41,23 +44,23 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget {
spacing: LayoutConstants.largePadding,
children: [
Text(
- 'Reader Settings',
+ l.readerSettings,
style: Theme.of(context).textTheme.titleLarge,
),
ChoiceOption(
- title: 'Reading Direction',
+ title: l.readingDirection,
icon: settings.readDirection == .leftToRight
? LucideIcons.chevronsRight
: LucideIcons.chevronsLeft,
- options: const [
+ options: [
ChoiceOptionEntry(
value: .leftToRight,
- label: 'Left to Right',
+ label: l.leftToRight,
icon: LucideIcons.chevronsRight,
),
ChoiceOptionEntry(
value: .rightToLeft,
- label: 'Right to Left',
+ label: l.rightToLeft,
icon: LucideIcons.chevronsLeft,
),
],
@@ -69,27 +72,27 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget {
},
),
ChoiceOption(
- title: 'Reader Mode',
+ title: l.readerMode,
icon: switch (settings.readerMode) {
.horizontal => LucideIcons.moveHorizontal,
.vertical => LucideIcons.moveVertical,
.spread => LucideIcons.columns2,
},
options: [
- const ChoiceOptionEntry(
+ ChoiceOptionEntry(
value: .horizontal,
- label: 'Horizontal',
+ label: l.horizontal,
icon: LucideIcons.moveHorizontal,
),
- const ChoiceOptionEntry(
+ ChoiceOptionEntry(
value: .vertical,
- label: 'Vertical',
+ label: l.vertical,
icon: LucideIcons.moveVertical,
),
if (breakpoint != .compact)
- const ChoiceOptionEntry(
+ ChoiceOptionEntry(
value: .spread,
- label: 'Two Page',
+ label: l.twoPage,
icon: LucideIcons.columns2,
),
],
@@ -102,26 +105,26 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget {
),
if (settings.readerMode == .horizontal) ...[
ChoiceOption(
- title: 'Fit Direction',
+ title: l.fitDirection,
icon: switch (settings.scaleType) {
.fitWidth => KoverIcons.fitWidth,
.fitHeight => KoverIcons.fitHeight,
.contain => KoverIcons.fitContain,
},
- options: const [
+ options: [
ChoiceOptionEntry(
value: .contain,
- label: 'Contain',
+ label: l.contain,
icon: KoverIcons.fitContain,
),
ChoiceOptionEntry(
value: .fitWidth,
- label: 'Width',
+ label: l.width,
icon: KoverIcons.fitWidth,
),
ChoiceOptionEntry(
value: .fitHeight,
- label: 'Height',
+ label: l.height,
icon: KoverIcons.fitHeight,
),
],
@@ -137,7 +140,7 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget {
],
if (settings.readerMode == .vertical) ...[
NumericOption(
- title: 'Margins',
+ title: l.margins,
icon: LucideIcons.panelLeftDashed,
value: settings.verticalReaderPadding,
min: ImageReaderSettingsLimits
@@ -151,7 +154,7 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget {
.setVerticalReaderPadding(newValue),
),
NumericOption(
- title: 'Vertical Gap',
+ title: l.verticalGap,
icon: LucideIcons.unfoldVertical,
value: settings.verticalReaderGap,
min: ImageReaderSettingsLimits.verticalReaderGapMin,
@@ -164,7 +167,7 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget {
],
if (settings.readerMode == .spread) ...[
NumericOption(
- title: 'Page Gap',
+ title: l.pageGap,
icon: LucideIcons.unfoldHorizontal,
value: settings.spreadReaderGap,
min: ImageReaderSettingsLimits.spreadReaderGapMin,
@@ -176,9 +179,8 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget {
.setSpreadReaderGap(newValue),
),
BooleanOption(
- title: 'Cover Page',
- description:
- 'Treat the first page as the cover, showing it as a single page',
+ title: l.coverPage,
+ description: l.coverPageDescription,
icon: LucideIcons.bookImage,
value: settings.spreadCoverPage,
onChanged: (newValue) async => await ref
@@ -187,7 +189,7 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget {
),
],
BooleanOption(
- title: 'Ignore Safe Areas',
+ title: l.ignoreSafeAreas,
icon: KoverIcons.safeArea,
value: settings.ignoreSafeAreas,
onChanged: (newValue) async => await ref
@@ -195,7 +197,7 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget {
.setIgnoreSafeAreas(newValue),
),
BooleanOption(
- title: 'Show Progress Bar',
+ title: l.showProgressBar,
icon: KoverIcons.progressBar,
value: settings.showProgressBar,
onChanged: (newValue) async => await ref
@@ -225,7 +227,7 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget {
onPressed: () async =>
await ref.read(provider.notifier).setDefault(),
icon: const Icon(LucideIcons.save),
- label: const Text('Set Defaults'),
+ label: Text(l.setDefaults),
),
),
Expanded(
@@ -233,7 +235,7 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget {
onPressed: () async =>
await ref.read(provider.notifier).reset(),
icon: const Icon(LucideIcons.rotateCcw),
- label: const Text('Reset'),
+ label: Text(l.reset),
),
),
],
diff --git a/lib/pages/reader/overlay/reader_controls.dart b/lib/pages/reader/overlay/reader_controls.dart
index 3ace2ebb..c9e2818a 100644
--- a/lib/pages/reader/overlay/reader_controls.dart
+++ b/lib/pages/reader/overlay/reader_controls.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/pages/reader/epub_reader/epub_reader_controls.dart';
import 'package:kover/pages/reader/image_reader/image_reader_controls.dart';
import 'package:kover/pages/reader/overlay/page_slider.dart';
@@ -85,9 +86,10 @@ class ReaderSettingsButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final l = AppLocalizations.of(context);
return IconButton(
icon: const Icon(LucideIcons.slidersHorizontal),
- tooltip: 'Reader Settings',
+ tooltip: l.readerSettings,
onPressed: () {
showModalBottomSheet(
context: context,
diff --git a/lib/pages/reader/overlay/reader_overlay.dart b/lib/pages/reader/overlay/reader_overlay.dart
index 75403ec3..97315e83 100644
--- a/lib/pages/reader/overlay/reader_overlay.dart
+++ b/lib/pages/reader/overlay/reader_overlay.dart
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/pages/reader/overlay/reader_controls.dart';
import 'package:kover/pages/reader/overlay/reader_header.dart';
import 'package:kover/riverpod/providers/reader.dart';
@@ -60,6 +61,7 @@ class ReaderOverlay extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final uiVisible = useState(false);
final snackbarDismissed = useState(false);
final showSnackbar = useState(ShowSnackbar.none);
@@ -68,7 +70,6 @@ class ReaderOverlay extends HookConsumerWidget {
chapterId: chapterId,
readingListId: readingListId,
);
-
final shouldShowSnackbar =
showSnackbar.value != ShowSnackbar.none &&
(!snackbarDismissed.value || uiVisible.value);
@@ -210,7 +211,9 @@ class ReaderOverlay extends HookConsumerWidget {
alignment: .bottomCenter,
child:
ChapterSnackbar(
- title: 'Previous: ${prevChapter.value?.title}',
+ title: l.previousChapter(
+ prevChapter.value?.title ?? '',
+ ),
onNavigate: () {
log.debug(
'navigating to previous chapter',
@@ -246,7 +249,9 @@ class ReaderOverlay extends HookConsumerWidget {
alignment: .bottomCenter,
child:
ChapterSnackbar(
- title: 'Next: ${nextChapter.value?.title}',
+ title: l.nextChapter(
+ nextChapter.value?.title ?? '',
+ ),
onNavigate: () {
log.debug(
'navigating to next chapter',
@@ -400,6 +405,7 @@ class ChapterSnackbar extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final l = AppLocalizations.of(context);
return SafeArea(
child: Card.filled(
margin: LayoutConstants.mediumEdgeInsets,
@@ -416,10 +422,10 @@ class ChapterSnackbar extends StatelessWidget {
),
),
if (onDismiss != null)
- TextButton(onPressed: onDismiss, child: const Text('Dismiss')),
+ TextButton(onPressed: onDismiss, child: Text(l.dismiss)),
FilledButton(
onPressed: onNavigate,
- child: const Text('Go'),
+ child: Text(l.go),
),
],
),
diff --git a/lib/pages/reader/pdf_reader/pdf_reader_controls.dart b/lib/pages/reader/pdf_reader/pdf_reader_controls.dart
index 08b53895..559f248a 100644
--- a/lib/pages/reader/pdf_reader/pdf_reader_controls.dart
+++ b/lib/pages/reader/pdf_reader/pdf_reader_controls.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/models/read_direction.dart';
import 'package:kover/riverpod/providers/settings/pdf_reader_settings.dart';
import 'package:kover/utils/constants/kover_icons.dart';
@@ -14,6 +15,7 @@ class PdfReaderSettingsBottomSheet extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final provider = pdfReaderSettingsProvider(seriesId: seriesId);
return Async(
@@ -36,25 +38,25 @@ class PdfReaderSettingsBottomSheet extends ConsumerWidget {
spacing: LayoutConstants.largePadding,
children: [
Text(
- 'Reader Settings',
+ l.readerSettings,
style: Theme.of(context).textTheme.titleLarge,
),
ChoiceOption(
- title: 'Reading Direction',
+ title: l.readingDirection,
icon: switch (settings.readDirection) {
.leftToRight => KoverIcons.readingDirectionLTR,
.rightToLeft => KoverIcons.readingDirectionRTL,
},
value: settings.readDirection,
- options: const [
+ options: [
ChoiceOptionEntry(
value: .leftToRight,
- label: 'Left To Right',
+ label: l.leftToRight,
icon: KoverIcons.readingDirectionLTR,
),
ChoiceOptionEntry(
value: .rightToLeft,
- label: 'Right To Left',
+ label: l.rightToLeft,
icon: KoverIcons.readingDirectionRTL,
),
],
@@ -65,22 +67,22 @@ class PdfReaderSettingsBottomSheet extends ConsumerWidget {
},
),
ChoiceOption(
- title: 'Reader Mode',
+ title: l.readerMode,
icon: switch (settings.readerMode) {
PdfReaderMode.vertical => KoverIcons.verticalReader,
PdfReaderMode.horizontal =>
KoverIcons.horizontalReader,
},
value: settings.readerMode,
- options: const [
+ options: [
ChoiceOptionEntry(
value: .vertical,
- label: 'Vertical',
+ label: l.vertical,
icon: KoverIcons.verticalReader,
),
ChoiceOptionEntry(
value: .horizontal,
- label: 'Horizontal',
+ label: l.horizontal,
icon: KoverIcons.horizontalReader,
),
],
@@ -91,7 +93,7 @@ class PdfReaderSettingsBottomSheet extends ConsumerWidget {
},
),
BooleanOption(
- title: 'Ignore Safe Areas',
+ title: l.ignoreSafeAreas,
icon: KoverIcons.safeArea,
value: settings.ignoreSafeAreas,
onChanged: (newValue) async {
@@ -101,7 +103,7 @@ class PdfReaderSettingsBottomSheet extends ConsumerWidget {
},
),
BooleanOption(
- title: 'Show Progress Bar',
+ title: l.showProgressBar,
icon: KoverIcons.progressBar,
value: settings.showProgressBar,
onChanged: (newValue) async {
@@ -133,7 +135,7 @@ class PdfReaderSettingsBottomSheet extends ConsumerWidget {
onPressed: () async =>
await ref.read(provider.notifier).setDefault(),
icon: const Icon(KoverIcons.save),
- label: const Text('Set Defaults'),
+ label: Text(l.setDefaults),
),
),
Expanded(
@@ -141,7 +143,7 @@ class PdfReaderSettingsBottomSheet extends ConsumerWidget {
onPressed: () async =>
await ref.read(provider.notifier).reset(),
icon: const Icon(KoverIcons.reset),
- label: const Text('Reset'),
+ label: Text(l.reset),
),
),
],
diff --git a/lib/pages/reader/pdf_reader/pdf_toc_drawer.dart b/lib/pages/reader/pdf_reader/pdf_toc_drawer.dart
index c305adef..6876e395 100644
--- a/lib/pages/reader/pdf_reader/pdf_toc_drawer.dart
+++ b/lib/pages/reader/pdf_reader/pdf_toc_drawer.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/riverpod/providers/reader/reader_navigation.dart';
import 'package:kover/utils/layout_constants.dart';
import 'package:kover/widgets/util/async_value.dart';
@@ -21,6 +22,7 @@ class PdfTocDrawer extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final selectedKey = useState(null);
final hasScrolled = useState(false);
@@ -66,7 +68,7 @@ class PdfTocDrawer extends HookConsumerWidget {
horizontal: LayoutConstants.mediumPadding,
),
child: Text(
- 'Table of Contents',
+ l.tableOfContents,
style: Theme.of(context).textTheme.headlineMedium,
),
),
diff --git a/lib/pages/reader/reader_page.dart b/lib/pages/reader/reader_page.dart
index 8b4fb9da..962843b1 100644
--- a/lib/pages/reader/reader_page.dart
+++ b/lib/pages/reader/reader_page.dart
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/pages/reader/epub_reader/epub_reader.dart';
import 'package:kover/pages/reader/image_reader/image_reader.dart';
import 'package:kover/pages/reader/pdf_reader/pdf_reader.dart';
@@ -33,6 +34,8 @@ class ReaderPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
+
useEffect(() {
SystemChrome.setEnabledSystemUIMode(.immersiveSticky);
@@ -88,10 +91,12 @@ class ReaderPage extends HookConsumerWidget {
size: LayoutConstants.largeIcon,
color: Theme.of(context).colorScheme.error,
),
- Text('Unsupported format: ${data.series.format}'),
+ Text(
+ l.unsupportedFormat(data.series.format.name.toUpperCase()),
+ ),
FilledButton(
onPressed: () => context.pop(),
- child: const Text('Back'),
+ child: Text(l.back),
),
],
),
diff --git a/lib/pages/reading_list_details_page/reading_list_app_bar.dart b/lib/pages/reading_list_details_page/reading_list_app_bar.dart
index 02409bc8..c7bbd08f 100644
--- a/lib/pages/reading_list_details_page/reading_list_app_bar.dart
+++ b/lib/pages/reading_list_details_page/reading_list_app_bar.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/riverpod/providers/reader.dart';
import 'package:kover/riverpod/providers/reading_lists.dart';
import 'package:kover/riverpod/providers/router.dart';
@@ -19,6 +20,7 @@ class ReadingListAppBar extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final readingList = ref.watch(
readingListProvider(readingListId: readingListId),
);
@@ -49,9 +51,7 @@ class ReadingListAppBar extends HookConsumerWidget {
if (readingList.summary != null)
Async(
asyncValue: chapterCount,
- data: (count) => Text(
- '$count ${count == 1 ? 'item' : 'items'}',
- ),
+ data: (count) => Text(l.items(count)),
),
],
),
diff --git a/lib/pages/reading_list_details_page/reading_list_chapter_entry.dart b/lib/pages/reading_list_details_page/reading_list_chapter_entry.dart
index c61000d6..755ee8d0 100644
--- a/lib/pages/reading_list_details_page/reading_list_chapter_entry.dart
+++ b/lib/pages/reading_list_details_page/reading_list_chapter_entry.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/models/chapter_model.dart';
import 'package:kover/riverpod/providers/chapter.dart';
import 'package:kover/riverpod/providers/router.dart';
@@ -21,6 +22,7 @@ class ReadingListChapterEntry extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final series = ref.watch(seriesProvider(seriesId: chapter.seriesId));
final progress = ref.watch(
chapterProgressProvider(chapterId: chapter.id),
@@ -32,7 +34,7 @@ class ReadingListChapterEntry extends ConsumerWidget {
contextMenu: ContextMenu(
entries: [
MenuItem(
- label: const Text('Go to chapter'),
+ label: Text(l.goToChapter),
icon: const Icon(KoverIcons.chapter),
onSelected: (_) {
ChapterDetailRoute(
@@ -42,7 +44,7 @@ class ReadingListChapterEntry extends ConsumerWidget {
},
),
MenuItem(
- label: const Text('Go to series'),
+ label: Text(l.goToSeries),
icon: const Icon(KoverIcons.series),
onSelected: (_) {
SeriesDetailRoute(seriesId: series.id).push(context);
diff --git a/lib/pages/reading_lists_page/reading_lists_page.dart b/lib/pages/reading_lists_page/reading_lists_page.dart
index 8da9fd03..e839b605 100644
--- a/lib/pages/reading_lists_page/reading_lists_page.dart
+++ b/lib/pages/reading_lists_page/reading_lists_page.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/models/enums/sort_direction.dart';
import 'package:kover/models/reading_list_model.dart';
import 'package:kover/riverpod/managers/sync_manager.dart';
@@ -19,6 +20,7 @@ class ReadingListsPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final sortDirection = useState(SortDirection.ascending);
final controller = useTextEditingController();
final readingLists = ref.watch(readingListsProvider);
@@ -34,7 +36,7 @@ class ReadingListsPage extends HookConsumerWidget {
keyboardDismissBehavior: .onDrag,
slivers: [
SliverAppBar.large(
- title: const Text('Reading Lists'),
+ title: Text(l.readingLists),
actionsPadding: const EdgeInsets.symmetric(
horizontal: LayoutConstants.smallPadding,
),
@@ -45,7 +47,7 @@ class ReadingListsPage extends HookConsumerWidget {
? KoverIcons.ascending
: KoverIcons.descending,
),
- menu: _menu(sortDirection),
+ menu: _menu(sortDirection: sortDirection, context: context),
),
],
),
@@ -113,14 +115,16 @@ class ReadingListsPage extends HookConsumerWidget {
return sorted;
}
- ContextMenu _menu(
- ValueNotifier sortDirection,
- ) {
+ ContextMenu _menu({
+ required ValueNotifier sortDirection,
+ required BuildContext context,
+ }) {
+ final l = AppLocalizations.of(context);
return ContextMenu(
entries: [
- const MenuHeader(text: 'Direction'),
+ MenuHeader(text: l.sortDirection),
MenuItem(
- label: const Text('Ascending'),
+ label: Text(l.ascending),
icon: _getItemIcon(
sortDirection.value == .ascending,
),
@@ -129,7 +133,7 @@ class ReadingListsPage extends HookConsumerWidget {
},
),
MenuItem(
- label: const Text('Descending'),
+ label: Text(l.descending),
icon: _getItemIcon(
sortDirection.value == .descending,
),
diff --git a/lib/pages/series_detail_page/chapters_page/chapters_page.dart b/lib/pages/series_detail_page/chapters_page/chapters_page.dart
index e10fe663..ff582020 100644
--- a/lib/pages/series_detail_page/chapters_page/chapters_page.dart
+++ b/lib/pages/series_detail_page/chapters_page/chapters_page.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/models/chapter_model.dart';
import 'package:kover/models/enums/sort_direction.dart';
import 'package:kover/riverpod/providers/series.dart';
@@ -19,6 +20,7 @@ class ChaptersPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final hideRead = useState(false);
final sortDirection = useState(SortDirection.ascending);
final chapters = ref.watch(
@@ -44,7 +46,7 @@ class ChaptersPage extends HookConsumerWidget {
: chapters;
return _ChaptersPage(
- title: 'Chapters',
+ title: l.chapters,
seriesId: seriesId,
chapters: toShow,
action: ContextMenuButton(
@@ -53,7 +55,11 @@ class ChaptersPage extends HookConsumerWidget {
? LucideIcons.arrowDownNarrowWide
: LucideIcons.arrowDownWideNarrow,
),
- menu: _getMenu(hideRead: hideRead, sortDirection: sortDirection),
+ menu: _getMenu(
+ hideRead: hideRead,
+ sortDirection: sortDirection,
+ context: context,
+ ),
),
);
}
@@ -65,6 +71,7 @@ class StorylinePage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final sortDirection = useState(SortDirection.ascending);
final chapters = ref.watch(
seriesDetailProvider(
@@ -79,7 +86,7 @@ class StorylinePage extends HookConsumerWidget {
: chapters;
return _ChaptersPage(
- title: 'Storyline',
+ title: l.storyline,
seriesId: seriesId,
chapters: toShow,
action: ContextMenuButton(
@@ -88,7 +95,7 @@ class StorylinePage extends HookConsumerWidget {
? LucideIcons.arrowDownNarrowWide
: LucideIcons.arrowDownWideNarrow,
),
- menu: _getMenu(sortDirection: sortDirection),
+ menu: _getMenu(sortDirection: sortDirection, context: context),
),
);
}
@@ -100,6 +107,7 @@ class SpecialsPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final sortDirection = useState(SortDirection.ascending);
final chapters = ref.watch(
seriesDetailProvider(
@@ -114,7 +122,7 @@ class SpecialsPage extends HookConsumerWidget {
: chapters;
return _ChaptersPage(
- title: 'Specials',
+ title: l.specials,
seriesId: seriesId,
chapters: toShow,
action: ContextMenuButton(
@@ -123,7 +131,7 @@ class SpecialsPage extends HookConsumerWidget {
? LucideIcons.arrowDownNarrowWide
: LucideIcons.arrowDownWideNarrow,
),
- menu: _getMenu(sortDirection: sortDirection),
+ menu: _getMenu(sortDirection: sortDirection, context: context),
),
);
}
@@ -197,31 +205,33 @@ class _ChaptersPage extends HookConsumerWidget {
ContextMenu _getMenu({
ValueNotifier? hideRead,
ValueNotifier? sortDirection,
+ required BuildContext context,
}) {
+ final l = AppLocalizations.of(context);
return ContextMenu(
entries: [
if (hideRead != null) ...[
- const MenuHeader(text: 'Filter'),
+ MenuHeader(text: l.filter),
MenuItem(
icon: hideRead.value ? const Icon(LucideIcons.check) : null,
- label: const Text('Hide Read'),
+ label: Text(l.hideRead),
onSelected: (_) => hideRead.value = !hideRead.value,
),
],
if (sortDirection != null) ...[
- const MenuHeader(text: 'Sort Direction'),
+ MenuHeader(text: l.sortBy),
MenuItem(
icon: sortDirection.value == SortDirection.ascending
? const Icon(LucideIcons.check)
: null,
- label: const Text('Ascending'),
+ label: Text(l.ascending),
onSelected: (_) => sortDirection.value = SortDirection.ascending,
),
MenuItem(
icon: sortDirection.value == SortDirection.descending
? const Icon(LucideIcons.check)
: null,
- label: const Text('Descending'),
+ label: Text(l.descending),
onSelected: (_) => sortDirection.value = SortDirection.descending,
),
],
diff --git a/lib/pages/series_detail_page/series_app_bar.dart b/lib/pages/series_detail_page/series_app_bar.dart
index af882ee8..80eb9b44 100644
--- a/lib/pages/series_detail_page/series_app_bar.dart
+++ b/lib/pages/series_detail_page/series_app_bar.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/models/series_model.dart';
import 'package:kover/riverpod/managers/download_manager.dart';
import 'package:kover/riverpod/managers/sync_manager.dart';
@@ -174,6 +175,7 @@ class _Metadata extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final metadata = ref.watch(seriesMetadataProvider(seriesId: series.id));
return Async(
asyncValue: metadata,
@@ -204,7 +206,7 @@ class _Metadata extends ConsumerWidget {
alignment: .spaceBetween,
children: [
LimitedList(
- title: 'Writers',
+ title: l.writers,
items: metadata.writers
.map(
(w) => Text(
diff --git a/lib/pages/series_detail_page/series_detail_page.dart b/lib/pages/series_detail_page/series_detail_page.dart
index 7f2bb418..cb2ed677 100644
--- a/lib/pages/series_detail_page/series_detail_page.dart
+++ b/lib/pages/series_detail_page/series_detail_page.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/pages/menu_page/app_list_tile.dart';
import 'package:kover/pages/series_detail_page/series_app_bar.dart';
import 'package:kover/riverpod/providers/router.dart';
@@ -16,6 +17,7 @@ class SeriesDetailPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final details = ref.watch(seriesDetailProvider(seriesId: seriesId));
final summary = ref.watch(
seriesMetadataProvider(seriesId: seriesId).select(
@@ -43,27 +45,30 @@ class SeriesDetailPage extends HookConsumerWidget {
children: [
if (detailsData.specials.isNotEmpty)
AppListTile(
- title: 'Specials (${detailsData.specials.length})',
+ title:
+ '${l.specials} (${detailsData.specials.length})',
onTap: () => SpecialsRoute(seriesId: seriesId).push(
context,
),
),
if (detailsData.storyline.isNotEmpty)
AppListTile(
- title: 'Storyline (${detailsData.storyline.length})',
+ title:
+ '${l.storyline} (${detailsData.storyline.length})',
onTap: () => StorylineRoute(
seriesId: seriesId,
).push(context),
),
if (detailsData.volumes.isNotEmpty)
AppListTile(
- title: 'Volumes (${detailsData.volumes.length})',
+ title: '${l.volumes} (${detailsData.volumes.length})',
onTap: () =>
VolumesRoute(seriesId: seriesId).push(context),
),
if (detailsData.chapters.isNotEmpty)
AppListTile(
- title: 'Chapters (${detailsData.chapters.length})',
+ title:
+ '${l.chapters} (${detailsData.chapters.length})',
onTap: () =>
ChaptersRoute(seriesId: seriesId).push(context),
),
@@ -100,6 +105,7 @@ class _Genres extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final metadata = ref.watch(seriesMetadataProvider(seriesId: seriesId));
final theme = Theme.of(context);
return Async(
@@ -109,7 +115,7 @@ class _Genres extends ConsumerWidget {
spacing: LayoutConstants.smallPadding,
children: [
Text(
- 'Genres',
+ l.genres,
style: Theme.of(context).textTheme.headlineSmall,
),
Wrap(
diff --git a/lib/pages/series_detail_page/volume_detail_page/volume_detail_page.dart b/lib/pages/series_detail_page/volume_detail_page/volume_detail_page.dart
index 8cb4345f..fe52d834 100644
--- a/lib/pages/series_detail_page/volume_detail_page/volume_detail_page.dart
+++ b/lib/pages/series_detail_page/volume_detail_page/volume_detail_page.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/pages/series_detail_page/volume_detail_page/volume_app_bar.dart';
import 'package:kover/riverpod/providers/volume.dart';
import 'package:kover/utils/layout_constants.dart';
@@ -17,6 +18,7 @@ class VolumeDetailPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final volume = ref.watch(volumeProvider(volumeId: volumeId)).value;
if (volume == null) return const SizedBox.shrink();
@@ -51,7 +53,7 @@ class VolumeDetailPage extends ConsumerWidget {
),
sliver: SliverToBoxAdapter(
child: Text(
- 'Chapters',
+ l.chapters,
style: Theme.of(context).textTheme.headlineSmall,
),
),
diff --git a/lib/pages/series_detail_page/volumes_page/volumes_page.dart b/lib/pages/series_detail_page/volumes_page/volumes_page.dart
index 6a9bbc19..f3f76f5e 100644
--- a/lib/pages/series_detail_page/volumes_page/volumes_page.dart
+++ b/lib/pages/series_detail_page/volumes_page/volumes_page.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/models/enums/sort_direction.dart';
import 'package:kover/riverpod/providers/series.dart';
import 'package:kover/utils/layout_constants.dart';
@@ -18,6 +19,7 @@ class VolumesPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final hideRead = useState(false);
final sortDirection = useState(SortDirection.ascending);
final controller = useTextEditingController();
@@ -31,13 +33,17 @@ class VolumesPage extends HookConsumerWidget {
keyboardDismissBehavior: .onDrag,
slivers: [
SliverAppBar.large(
- title: const Text('Volumes'),
+ title: Text(l.volumes),
actionsPadding: const EdgeInsets.symmetric(
horizontal: LayoutConstants.smallPadding,
),
actions: [
ContextMenuButton(
- menu: _getMenu(hideRead, sortDirection),
+ menu: _getMenu(
+ hideRead: hideRead,
+ sortDirection: sortDirection,
+ context: context,
+ ),
icon: Icon(
sortDirection.value == .ascending
? LucideIcons.arrowDownNarrowWide
@@ -70,31 +76,33 @@ class VolumesPage extends HookConsumerWidget {
);
}
- ContextMenu _getMenu(
- ValueNotifier hideRead,
- ValueNotifier sortDirection,
- ) {
+ ContextMenu _getMenu({
+ required ValueNotifier hideRead,
+ required ValueNotifier sortDirection,
+ required BuildContext context,
+ }) {
+ final l = AppLocalizations.of(context);
return ContextMenu(
entries: [
- const MenuHeader(text: 'Filter'),
+ MenuHeader(text: l.filter),
MenuItem(
icon: hideRead.value ? const Icon(LucideIcons.check) : null,
- label: const Text('Hide Read'),
+ label: Text(l.hideRead),
onSelected: (_) => hideRead.value = !hideRead.value,
),
- const MenuHeader(text: 'Sort Direction'),
+ MenuHeader(text: l.sortDirection),
MenuItem(
icon: sortDirection.value == SortDirection.ascending
? const Icon(LucideIcons.check)
: null,
- label: const Text('Ascending'),
+ label: Text(l.ascending),
onSelected: (_) => sortDirection.value = SortDirection.ascending,
),
MenuItem(
icon: sortDirection.value == SortDirection.descending
? const Icon(LucideIcons.check)
: null,
- label: const Text('Descending'),
+ label: Text(l.descending),
onSelected: (_) => sortDirection.value = SortDirection.descending,
),
],
diff --git a/lib/pages/series_page/series_page.dart b/lib/pages/series_page/series_page.dart
index 39e35fee..94a1ed79 100644
--- a/lib/pages/series_page/series_page.dart
+++ b/lib/pages/series_page/series_page.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/models/enums/series_sort_option.dart';
import 'package:kover/models/enums/sort_direction.dart';
import 'package:kover/riverpod/providers/collections.dart';
@@ -20,7 +21,8 @@ class AllSeriesPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return const SeriesPage(title: 'All Series');
+ final l = AppLocalizations.of(context);
+ return SeriesPage(title: l.allSeries);
}
}
@@ -132,7 +134,11 @@ class SeriesPage extends HookConsumerWidget {
? LucideIcons.arrowDownNarrowWide
: LucideIcons.arrowDownWideNarrow,
),
- menu: _menu(sortOption, sortDirection),
+ menu: _menu(
+ sortOption: sortOption,
+ sortDirection: sortDirection,
+ context: context,
+ ),
),
],
),
@@ -167,44 +173,46 @@ class SeriesPage extends HookConsumerWidget {
);
}
- ContextMenu _menu(
- ValueNotifier sortOption,
- ValueNotifier sortDirection,
- ) {
+ ContextMenu _menu({
+ required ValueNotifier sortOption,
+ required ValueNotifier sortDirection,
+ required BuildContext context,
+ }) {
+ final l = AppLocalizations.of(context);
return ContextMenu(
entries: [
- const MenuHeader(text: 'Sort by'),
+ MenuHeader(text: l.sortBy),
MenuItem(
- label: const Text('Name'),
+ label: Text(l.name),
icon: _getItemIcon(sortOption.value == .name),
onSelected: (_) {
sortOption.value = .name;
},
),
MenuItem(
- label: const Text('Date Added'),
+ label: Text(l.dateAdded),
icon: _getItemIcon(sortOption.value == .dateAdded),
onSelected: (_) {
sortOption.value = .dateAdded;
},
),
MenuItem(
- label: const Text('Last Modified'),
+ label: Text(l.lastModified),
icon: _getItemIcon(sortOption.value == .lastModified),
onSelected: (_) {
sortOption.value = .lastModified;
},
),
- const MenuHeader(text: 'Direction'),
+ MenuHeader(text: l.sortDirection),
MenuItem(
- label: const Text('Ascending'),
+ label: Text(l.ascending),
icon: _getItemIcon(sortDirection.value == .ascending),
onSelected: (_) {
sortDirection.value = .ascending;
},
),
MenuItem(
- label: const Text('Descending'),
+ label: Text(l.descending),
icon: _getItemIcon(sortDirection.value == .descending),
onSelected: (_) {
sortDirection.value = .descending;
diff --git a/lib/pages/settings/credentials_settings.dart b/lib/pages/settings/credentials_settings.dart
index 478204eb..5ef90b9f 100644
--- a/lib/pages/settings/credentials_settings.dart
+++ b/lib/pages/settings/credentials_settings.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/riverpod/providers/auth.dart';
import 'package:kover/riverpod/providers/server_settings.dart';
import 'package:kover/riverpod/providers/settings/credentials.dart';
@@ -40,6 +41,7 @@ class _CredentialsForm extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final obscureKey = useState(true);
final urlController = useTextEditingController(text: data.url ?? '');
final apiKeyController = useTextEditingController(text: data.apiKey ?? '');
@@ -50,14 +52,14 @@ class _CredentialsForm extends HookConsumerWidget {
spacing: LayoutConstants.mediumPadding,
children: [
Text(
- 'Credentials',
+ l.credentials,
style: Theme.of(context).textTheme.headlineSmall,
),
TextField(
enabled: loginStatus != .loading,
controller: urlController,
- decoration: const InputDecoration(
- labelText: 'Base URL',
+ decoration: InputDecoration(
+ labelText: l.baseUrl,
),
),
TextField(
@@ -65,7 +67,7 @@ class _CredentialsForm extends HookConsumerWidget {
enabled: loginStatus != .loading,
controller: apiKeyController,
decoration: InputDecoration(
- labelText: 'API Key',
+ labelText: l.apiKey,
suffixIcon: Padding(
padding: const EdgeInsetsGeometry.symmetric(
horizontal: LayoutConstants.smallPadding,
@@ -99,7 +101,7 @@ class _CredentialsForm extends HookConsumerWidget {
),
);
},
- label: const Text('Save'),
+ label: Text(l.save),
icon: const Icon(LucideIcons.save),
),
],
@@ -116,6 +118,7 @@ class _User extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
return switch (loginStatus) {
LoginStatus.noCredentials => const SizedBox.shrink(),
LoginStatus.loading => const SizedBox.square(
@@ -130,7 +133,7 @@ class _User extends ConsumerWidget {
color: Theme.of(context).colorScheme.error,
),
Text(
- 'Invalid credentials',
+ l.invalidCredentials,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
diff --git a/lib/pages/settings/data_management_settings.dart b/lib/pages/settings/data_management_settings.dart
index 215bca17..1f27a7fa 100644
--- a/lib/pages/settings/data_management_settings.dart
+++ b/lib/pages/settings/data_management_settings.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/riverpod/providers/settings/download_settings.dart';
import 'package:kover/riverpod/repository/database.dart';
import 'package:kover/utils/layout_constants.dart';
@@ -14,6 +15,7 @@ class DataManagementSettings extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final theme = Theme.of(context);
final settings = ref.watch(downloadSettingsProvider);
@@ -29,14 +31,12 @@ class DataManagementSettings extends ConsumerWidget {
spacing: LayoutConstants.largePadding,
children: [
Text(
- 'Data Management',
+ l.dataManagement,
style: Theme.of(context).textTheme.headlineSmall,
),
BooleanOption(
- title: 'Download All Covers',
- description:
- 'If disabled, covers will only be downloaded together with chapters. '
- 'Covers will still be fetched from the server on demand when not downloaded and a connection is available.',
+ title: l.downloadAllCovers,
+ description: l.downloadAllCoversDescription,
icon: LucideIcons.imageDownDir,
value: data.downloadCovers,
onChanged: (value) async {
@@ -46,7 +46,7 @@ class DataManagementSettings extends ConsumerWidget {
},
),
NumericOption(
- title: 'Max Concurrent Downloads',
+ title: l.maxConcurrentDownloads,
icon: LucideIcons.download,
min: 1,
max: 10,
@@ -70,7 +70,7 @@ class DataManagementSettings extends ConsumerWidget {
children: [
DatabaseClearOperationButton(
asyncValue: ref.watch(reclaimSpaceProvider),
- text: 'Reclaim Space',
+ text: l.reclaimSpace,
icon: const Icon(LucideIcons.databaseZap),
onPressed: () async {
await ref
@@ -80,7 +80,7 @@ class DataManagementSettings extends ConsumerWidget {
),
DatabaseClearOperationButton(
asyncValue: ref.watch(clearDownloadsProvider),
- text: 'Clear Downloads',
+ text: l.clearDownloads,
icon: const Icon(Icons.file_download_off),
onPressed: () async {
await ref
@@ -90,7 +90,7 @@ class DataManagementSettings extends ConsumerWidget {
),
DatabaseClearOperationButton(
asyncValue: ref.watch(clearCoversProvider),
- text: 'Clear Covers',
+ text: l.clearCovers,
icon: const Icon(LucideIcons.imageOff),
onPressed: () async {
await ref
@@ -100,23 +100,21 @@ class DataManagementSettings extends ConsumerWidget {
),
DatabaseClearOperationButton(
asyncValue: ref.watch(clearDatabaseProvider),
- text: 'Clear Database',
+ text: l.clearDatabase,
icon: const Icon(LucideIcons.trash),
onPressed: () async {
final confirmed = await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
- title: const Text('Are you sure?'),
- content: const Text(
- 'This will clear the entire local database, including any unsynced progress and downloaded data. This action cannot be undone.',
- ),
+ title: Text(l.clearDatabaseDialogTitle),
+ content: Text(l.clearDatabaseDialogContent),
actions: [
TextButton(
onPressed: () => Navigator.of(
context,
).pop(false),
- child: const Text('Cancel'),
+ child: Text(l.cancel),
),
FilledButton(
style: ElevatedButton.styleFrom(
@@ -128,9 +126,7 @@ class DataManagementSettings extends ConsumerWidget {
onPressed: () => Navigator.of(
context,
).pop(true),
- child: const Text(
- 'Clear Database',
- ),
+ child: Text(l.clearDatabase),
),
],
);
@@ -180,6 +176,8 @@ class DatabaseClearOperationButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
+
return Async(
asyncValue: asyncValue,
data: (status) {
@@ -192,7 +190,7 @@ class DatabaseClearOperationButton extends ConsumerWidget {
label: Text(text),
),
.busy => Tooltip(
- message: 'Database busy...',
+ message: l.databaseBusy,
triggerMode: .tap,
child: FilledButton.icon(
onPressed: null,
@@ -250,11 +248,16 @@ class DatabaseSize extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
+
return Row(
mainAxisSize: .min,
mainAxisAlignment: .start,
children: [
- Text('Database Size: ', style: Theme.of(context).textTheme.labelMedium),
+ Text(
+ '${l.databaseSize}: ',
+ style: Theme.of(context).textTheme.labelMedium,
+ ),
Async(
asyncValue: ref.watch(databaseSizeProvider),
data: (size) {
diff --git a/lib/pages/settings/general_settings.dart b/lib/pages/settings/general_settings.dart
index 61ccf9ca..d406a631 100644
--- a/lib/pages/settings/general_settings.dart
+++ b/lib/pages/settings/general_settings.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/riverpod/providers/settings/general_settings.dart';
import 'package:kover/riverpod/providers/theme.dart' hide Theme;
import 'package:kover/utils/constants/kover_icons.dart';
@@ -14,6 +15,7 @@ class GeneralSettings extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final theme = ref.watch(themeProvider);
final generalSettings = ref.watch(generalSettingsProvider);
@@ -29,26 +31,26 @@ class GeneralSettings extends ConsumerWidget {
spacing: LayoutConstants.largePadding,
children: [
Text(
- 'General',
+ l.general,
style: Theme.of(context).textTheme.headlineSmall,
),
ChoiceOption(
- title: 'Theme Mode',
+ title: l.themeMode,
icon: LucideIcons.palette,
- options: const [
+ options: [
ChoiceOptionEntry(
value: ThemeMode.system,
- label: 'System',
+ label: l.system,
icon: LucideIcons.sunMoon,
),
ChoiceOptionEntry(
value: ThemeMode.light,
- label: 'Light',
+ label: l.light,
icon: LucideIcons.sun,
),
ChoiceOptionEntry(
value: ThemeMode.dark,
- label: 'Dark',
+ label: l.dark,
icon: LucideIcons.moon,
),
],
@@ -58,7 +60,7 @@ class GeneralSettings extends ConsumerWidget {
},
),
BooleanOption(
- title: 'Outlined Theme',
+ title: l.outlinedTheme,
icon: LucideIcons.squareDashed,
value: theme.outlined,
onChanged: (value) =>
@@ -67,13 +69,9 @@ class GeneralSettings extends ConsumerWidget {
Async(
asyncValue: generalSettings,
data: (generalSettings) => BooleanOption(
- title: 'Send anonymous crash reports and diagnostics',
+ title: l.sendDiagnostics,
icon: KoverIcons.analytics,
- description:
- 'Help improve the app by sending anonymous error and '
- 'performance statistics. The data does not contain any '
- 'personal information and is uniquely used to improve '
- 'the app.',
+ description: l.sendDiagnosticsDescription,
value: generalSettings.sendDiagnostics,
onChanged: (value) => ref
.read(generalSettingsProvider.notifier)
diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart
index 0544781d..e6dee48e 100644
--- a/lib/pages/settings/settings_page.dart
+++ b/lib/pages/settings/settings_page.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/pages/settings/credentials_settings.dart';
import 'package:kover/pages/settings/data_management_settings.dart';
import 'package:kover/pages/settings/general_settings.dart';
@@ -11,22 +12,23 @@ class SettingsPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
- return const Scaffold(
+ final l = AppLocalizations.of(context);
+ return Scaffold(
extendBody: true,
body: SafeArea(
bottom: false,
child: CustomScrollView(
slivers: [
SliverAppBar.large(
- title: Text('Settings'),
+ title: Text(l.settings),
),
- SliverToBoxAdapter(child: CredentialsSettings()),
- SliverToBoxAdapter(child: GeneralSettings()),
- SliverToBoxAdapter(child: DataManagementSettings()),
- SliverToBoxAdapter(
+ const SliverToBoxAdapter(child: CredentialsSettings()),
+ const SliverToBoxAdapter(child: GeneralSettings()),
+ const SliverToBoxAdapter(child: DataManagementSettings()),
+ const SliverToBoxAdapter(
child: Center(child: VersionLabel()),
),
- SliverBottomPadding(),
+ const SliverBottomPadding(),
],
),
),
diff --git a/lib/pages/settings/version_label.dart b/lib/pages/settings/version_label.dart
index 7ab76cdb..98ca4b6c 100644
--- a/lib/pages/settings/version_label.dart
+++ b/lib/pages/settings/version_label.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/riverpod/providers/package_info.dart';
import 'package:kover/utils/layout_constants.dart';
import 'package:kover/widgets/util/async_value.dart';
@@ -10,6 +11,7 @@ class VersionLabel extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final theme = Theme.of(context);
final info = ref.watch(packageInfoProvider);
@@ -21,8 +23,7 @@ class VersionLabel extends ConsumerWidget {
showAboutDialog(
context: context,
applicationName: info.appName,
- applicationVersion:
- 'Version: ${info.version} (${info.buildNumber})',
+ applicationVersion: l.version(info.version, info.buildNumber),
applicationIcon: Container(
width: LayoutConstants.largestIcon,
height: LayoutConstants.largestIcon,
@@ -45,7 +46,7 @@ class VersionLabel extends ConsumerWidget {
children: [
Row(
children: [
- const Text('GitHub: '),
+ Text('${l.github}: '),
InkWell(
borderRadius: BorderRadius.circular(
LayoutConstants.smallBorderRadius,
@@ -71,7 +72,7 @@ class VersionLabel extends ConsumerWidget {
],
),
- Text('Made with ❤️', style: theme.textTheme.labelSmall),
+ Text(l.madeWithLove, style: theme.textTheme.labelSmall),
],
),
],
diff --git a/lib/pages/want_to_read_page/want_to_read_page.dart b/lib/pages/want_to_read_page/want_to_read_page.dart
index 7a395bfe..242f8b04 100644
--- a/lib/pages/want_to_read_page/want_to_read_page.dart
+++ b/lib/pages/want_to_read_page/want_to_read_page.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/riverpod/managers/sync_manager.dart';
import 'package:kover/riverpod/providers/want_to_read.dart';
import 'package:kover/utils/layout_constants.dart';
@@ -14,6 +15,7 @@ class WantToReadPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(syncManagerProvider.notifier).syncLibraries();
});
@@ -31,7 +33,7 @@ class WantToReadPage extends ConsumerWidget {
padding: LayoutConstants.smallEdgeInsets,
sliver: SliverToBoxAdapter(
child: Text(
- "Want to Read",
+ l.wantToRead,
style: Theme.of(context).textTheme.headlineMedium,
),
),
diff --git a/lib/utils/constants/kover_icons.dart b/lib/utils/constants/kover_icons.dart
index 25a02fb4..76d6ef34 100644
--- a/lib/utils/constants/kover_icons.dart
+++ b/lib/utils/constants/kover_icons.dart
@@ -28,6 +28,7 @@ sealed class KoverIcons {
static const IconData readingList = LucideIcons.layoutList;
static const IconData check = LucideIcons.check;
+ static const IconData info = LucideIcons.info;
static const IconData chevronRight = LucideIcons.chevronRight;
}
diff --git a/lib/widgets/actions_app_bar/search_button.dart b/lib/widgets/actions_app_bar/search_button.dart
index 8941da8e..a0af2c66 100644
--- a/lib/widgets/actions_app_bar/search_button.dart
+++ b/lib/widgets/actions_app_bar/search_button.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/models/chapter_model.dart';
import 'package:kover/models/series_model.dart';
import 'package:kover/models/volume_model.dart';
@@ -64,6 +65,7 @@ class SearchButton extends HookConsumerWidget {
);
},
suggestionsBuilder: (context, controller) async {
+ final l = AppLocalizations.of(context);
final theme = Theme.of(context);
final seriesResults = await ref.read(
searchSeriesProvider(controller.text).future,
@@ -84,7 +86,7 @@ class SearchButton extends HookConsumerWidget {
return [
if (seriesResults.isNotEmpty) ...[
Text(
- 'Series',
+ l.series,
style: theme.textTheme.headlineSmall,
),
...seriesResults.map(
@@ -96,7 +98,7 @@ class SearchButton extends HookConsumerWidget {
],
if (volumesResults.isNotEmpty) ...[
Text(
- 'Volumes',
+ l.volumes,
style: theme.textTheme.headlineSmall,
),
...volumesResults.map(
@@ -108,7 +110,7 @@ class SearchButton extends HookConsumerWidget {
],
if (chaptersResults.isNotEmpty) ...[
Text(
- 'Chapters',
+ l.chapters,
style: theme.textTheme.headlineSmall,
),
...chaptersResults.map(
diff --git a/lib/widgets/actions_app_bar/sync_button.dart b/lib/widgets/actions_app_bar/sync_button.dart
index a07ebb81..14aa63db 100644
--- a/lib/widgets/actions_app_bar/sync_button.dart
+++ b/lib/widgets/actions_app_bar/sync_button.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/riverpod/managers/sync_manager.dart';
import 'package:kover/utils/layout_constants.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
@@ -86,6 +87,7 @@ class SyncMenuOverlay extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final syncState = ref.watch(syncManagerProvider);
final entries =
@@ -93,7 +95,7 @@ class SyncMenuOverlay extends ConsumerWidget {
syncing: (phases) => [
for (final phase in phases)
(
- label: _phaseLabel(phase),
+ label: _phaseLabel(l, phase),
),
],
) ??
@@ -113,7 +115,7 @@ class SyncMenuOverlay extends ConsumerWidget {
vertical: LayoutConstants.smallPadding,
),
child: Text(
- 'No active sync operations',
+ l.noActiveSyncOperations,
style: Theme.of(context).textTheme.bodySmall,
),
),
@@ -144,22 +146,21 @@ class SyncMenuOverlay extends ConsumerWidget {
}
}
-String _phaseLabel(SyncPhase phase) {
+String _phaseLabel(AppLocalizations l, SyncPhase phase) {
return phase.when(
- allSeries: () => 'Syncing all series',
- metadata: () => 'Syncing metadata',
- tocs: () => 'Syncing chapters TOCs',
- recentlyAdded: () => 'Syncing recently added',
- recentlyUpdated: () => 'Syncing recently updated',
- libraries: () => 'Syncing libraries',
- progress: () => 'Syncing progress',
- covers: () => 'Syncing covers',
- collections: () => 'Syncing collections',
- readingLists: () => 'Syncing reading lists',
- refreshMetadata: (seriesId) => 'Refreshing metadata for series $seriesId',
- refreshCovers: (seriesId) => 'Refreshing covers for series $seriesId',
- refreshServerSettings: () => 'Refreshing server settings',
- refreshToc: (chapterId) =>
- 'Refreshing table of contents for chapter $chapterId',
+ allSeries: () => l.syncingAllSeries,
+ metadata: () => l.syncingMetadata,
+ tocs: () => l.syncingTocs,
+ recentlyAdded: () => l.syncingRecentlyAdded,
+ recentlyUpdated: () => l.syncingRecentlyUpdated,
+ libraries: () => l.syncingLibraries,
+ progress: () => l.syncingProgress,
+ covers: () => l.syncingCovers,
+ collections: () => l.syncingCollections,
+ readingLists: () => l.syncingReadingLists,
+ refreshMetadata: (seriesId) => l.refreshingMetadataForSeries(seriesId),
+ refreshCovers: (seriesId) => l.refreshingCoversForSeries(seriesId),
+ refreshServerSettings: () => l.refreshingServerSettings,
+ refreshToc: (chapterId) => l.refreshingChapterToc(chapterId),
);
}
diff --git a/lib/widgets/cards/cover_card.dart b/lib/widgets/cards/cover_card.dart
index 367f1fab..9dd4058c 100644
--- a/lib/widgets/cards/cover_card.dart
+++ b/lib/widgets/cards/cover_card.dart
@@ -3,13 +3,14 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/utils/layout_constants.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
class CoverCard extends ConsumerWidget {
final String? title;
final Icon? icon;
- final String actionLabel;
+ final String? actionLabel;
final Icon actionIcon;
final Icon? actionDisabledIcon;
final bool actionDisabled;
@@ -23,7 +24,7 @@ class CoverCard extends ConsumerWidget {
super.key,
this.title,
this.icon,
- this.actionLabel = 'Read',
+ this.actionLabel,
this.actionIcon = const Icon(LucideIcons.bookOpen),
this.actionDisabledIcon,
this.actionDisabled = true,
@@ -36,6 +37,9 @@ class CoverCard extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
+ final effectiveLabel = actionLabel ?? l.read;
+
return Card.filled(
clipBehavior: .antiAlias,
child: InkWell(
@@ -80,14 +84,16 @@ class CoverCard extends ConsumerWidget {
icon:
actionDisabledIcon ??
const Icon(LucideIcons.wifiOff),
- label: FittedBox(child: Text(actionLabel)),
+ label: FittedBox(
+ child: Text(effectiveLabel),
+ ),
onPressed: null,
),
),
)
: FilledButton.icon(
icon: actionIcon,
- label: FittedBox(child: Text(actionLabel)),
+ label: FittedBox(child: Text(effectiveLabel)),
onPressed: onActionTap,
),
),
diff --git a/lib/widgets/context_menu/actions_menu.dart b/lib/widgets/context_menu/actions_menu.dart
index 7ab9529d..5407b630 100644
--- a/lib/widgets/context_menu/actions_menu.dart
+++ b/lib/widgets/context_menu/actions_menu.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/utils/extensions/iterable.dart';
import 'package:kover/widgets/context_menu/context_menu_button.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
@@ -32,6 +33,7 @@ class ActionsContextMenu extends StatelessWidget {
Widget build(BuildContext context) {
return _LocalContextMenuRegion(
contextMenu: _getContextMenu(
+ context: context,
onMarkRead: onMarkRead,
onMarkUnread: onMarkUnread,
onAddWantToRead: onAddWantToRead,
@@ -70,6 +72,7 @@ class ActionsMenuButton extends StatelessWidget {
Widget build(BuildContext context) {
return ContextMenuButton(
menu: _getContextMenu(
+ context: context,
onMarkRead: onMarkRead,
onMarkUnread: onMarkUnread,
onDownload: onDownload,
@@ -101,6 +104,7 @@ class _LocalContextMenuRegion extends StatelessWidget {
}
ContextMenu _getContextMenu({
+ required BuildContext context,
VoidCallback? onMarkRead,
VoidCallback? onMarkUnread,
VoidCallback? onAddWantToRead,
@@ -111,18 +115,22 @@ ContextMenu _getContextMenu({
VoidCallback? onRefreshCovers,
}) {
final wantToReadEntries = _wantToReadEntries(
+ context: context,
onAddWantToRead: onAddWantToRead,
onRemoveWantToRead: onRemoveWantToRead,
);
final markReadEntries = _markReadEntries(
+ context: context,
onMarkRead: onMarkRead,
onMarkUnread: onMarkUnread,
);
final downloadEntries = _downloadEntries(
+ context: context,
onDownload: onDownload,
onRemoveDownload: onRemoveDownload,
);
final refreshEntries = _refreshEntries(
+ context: context,
onRefreshMetadata: onRefreshMetadata,
onRefreshCovers: onRefreshCovers,
);
@@ -147,19 +155,21 @@ List _withDividers(List> entries) {
}
List _wantToReadEntries({
+ required BuildContext context,
void Function()? onAddWantToRead,
void Function()? onRemoveWantToRead,
}) {
+ final l = AppLocalizations.of(context);
return [
if (onAddWantToRead != null)
MenuItem(
- label: const Text('Add to Want to Read'),
+ label: Text(l.addToWantToRead),
icon: const Icon(LucideIcons.star),
onSelected: (_) => onAddWantToRead(),
),
if (onRemoveWantToRead != null)
MenuItem(
- label: const Text('Remove from Want to Read'),
+ label: Text(l.removeFromWantToRead),
icon: const Icon(LucideIcons.starOff),
onSelected: (_) => onRemoveWantToRead(),
),
@@ -167,19 +177,21 @@ List _wantToReadEntries({
}
List _markReadEntries({
+ required BuildContext context,
void Function()? onMarkRead,
void Function()? onMarkUnread,
}) {
+ final l = AppLocalizations.of(context);
return [
if (onMarkRead != null)
MenuItem(
- label: const Text('Mark Read'),
+ label: Text(l.markAsRead),
icon: const Icon(LucideIcons.bookCheck),
onSelected: (_) => onMarkRead(),
),
if (onMarkUnread != null)
MenuItem(
- label: const Text('Mark Unread'),
+ label: Text(l.markAsUnread),
icon: const Icon(LucideIcons.bookX),
onSelected: (_) => onMarkUnread(),
),
@@ -187,19 +199,21 @@ List _markReadEntries({
}
List _downloadEntries({
+ required BuildContext context,
void Function()? onDownload,
void Function()? onRemoveDownload,
}) {
+ final l = AppLocalizations.of(context);
return [
if (onDownload != null)
MenuItem(
- label: const Text('Download'),
+ label: Text(l.download),
icon: const Icon(LucideIcons.download),
onSelected: (_) => onDownload(),
),
if (onRemoveDownload != null)
MenuItem(
- label: const Text('Remove Download'),
+ label: Text(l.removeDownload),
icon: const Icon(LucideIcons.trash2),
onSelected: (_) => onRemoveDownload(),
),
@@ -207,19 +221,21 @@ List _downloadEntries({
}
List _refreshEntries({
+ required BuildContext context,
VoidCallback? onRefreshMetadata,
VoidCallback? onRefreshCovers,
}) {
+ final l = AppLocalizations.of(context);
return [
if (onRefreshMetadata != null)
MenuItem(
- label: const Text('Refresh Metadata'),
+ label: Text(l.refreshMetadata),
icon: const Icon(LucideIcons.fileBracesCorner),
onSelected: (_) => onRefreshMetadata(),
),
if (onRefreshCovers != null)
MenuItem(
- label: const Text('Refresh Covers'),
+ label: Text(l.refreshCovers),
icon: const Icon(LucideIcons.imageDown),
onSelected: (_) => onRefreshCovers(),
),
diff --git a/lib/widgets/details/detail_app_bar.dart b/lib/widgets/details/detail_app_bar.dart
index c79fb925..5cade444 100644
--- a/lib/widgets/details/detail_app_bar.dart
+++ b/lib/widgets/details/detail_app_bar.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/pages/series_detail_page/series_info_background.dart';
import 'package:kover/utils/layout_constants.dart';
import 'package:kover/widgets/lists/adaptive_sliver_app_bar.dart';
@@ -130,6 +131,7 @@ class ContinuePointButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final theme = Theme.of(context);
return Card(
@@ -165,7 +167,7 @@ class ContinuePointButton extends ConsumerWidget {
mainAxisSize: .min,
children: [
Text(
- 'Continue Reading',
+ l.continueReading,
style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.onPrimaryContainer,
),
diff --git a/lib/widgets/details/filter_input_field.dart b/lib/widgets/details/filter_input_field.dart
index b4937bf7..0d3aa2fc 100644
--- a/lib/widgets/details/filter_input_field.dart
+++ b/lib/widgets/details/filter_input_field.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
class FilterInputField extends HookWidget {
@@ -12,11 +13,12 @@ class FilterInputField extends HookWidget {
@override
Widget build(BuildContext context) {
+ final l = AppLocalizations.of(context);
useListenable(controller);
return TextField(
controller: controller,
decoration: InputDecoration(
- hintText: 'Filter',
+ hintText: l.filter,
prefixIcon: const Icon(LucideIcons.listFilter),
suffixIcon: controller.text.isNotEmpty
? IconButton(
diff --git a/lib/widgets/details/info_widgets.dart b/lib/widgets/details/info_widgets.dart
index 24e0514d..5cf8df73 100644
--- a/lib/widgets/details/info_widgets.dart
+++ b/lib/widgets/details/info_widgets.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/riverpod/providers/want_to_read.dart';
-import 'package:kover/utils/extensions/int.dart';
import 'package:kover/utils/layout_constants.dart';
import 'package:kover/widgets/util/async_value.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
@@ -19,6 +19,7 @@ class LimitedList extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final l = AppLocalizations.of(context);
final display = items.take(maxItems);
return Column(
mainAxisSize: .min,
@@ -36,7 +37,7 @@ class LimitedList extends StatelessWidget {
for (final item in display) item,
if (display.length < items.length)
Text(
- '+${items.length - display.length} more',
+ l.moreCount(items.length - display.length),
style: Theme.of(context).textTheme.labelMedium,
),
],
@@ -82,6 +83,7 @@ class WordCount extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final l = AppLocalizations.of(context);
return Row(
mainAxisSize: .min,
spacing: LayoutConstants.smallPadding,
@@ -91,7 +93,7 @@ class WordCount extends StatelessWidget {
size: LayoutConstants.smallIcon,
),
Text(
- '${wordCount.prettyInt()} words',
+ l.wordCount(wordCount),
),
],
);
@@ -131,6 +133,7 @@ class RemainingHours extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final l = AppLocalizations.of(context);
return Row(
mainAxisSize: .min,
spacing: LayoutConstants.smallPadding,
@@ -140,7 +143,7 @@ class RemainingHours extends StatelessWidget {
size: LayoutConstants.smallIcon,
),
Text(
- '~${hours.toStringAsFixed(1)} hours',
+ l.hoursCount(hours),
),
],
);
@@ -156,6 +159,7 @@ class Pages extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final l = AppLocalizations.of(context);
return Row(
mainAxisSize: .min,
spacing: LayoutConstants.smallPadding,
@@ -164,7 +168,7 @@ class Pages extends StatelessWidget {
LucideIcons.fileStack,
size: LayoutConstants.smallIcon,
),
- Text('${pages.prettyInt()} pages'),
+ Text(l.pagesCount(pages)),
],
);
}
diff --git a/lib/widgets/details/summary.dart b/lib/widgets/details/summary.dart
index ae84abe4..80801278 100644
--- a/lib/widgets/details/summary.dart
+++ b/lib/widgets/details/summary.dart
@@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/utils/extensions/string.dart';
import 'package:kover/utils/layout_constants.dart';
@@ -17,6 +18,7 @@ class Summary extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final collapsed = useState(true);
if (summary == null || summary!.isEmpty) return const SizedBox.shrink();
@@ -30,13 +32,13 @@ class Summary extends HookConsumerWidget {
crossAxisAlignment: .center,
children: [
Text(
- 'Summary',
+ l.summary,
style: Theme.of(context).textTheme.headlineSmall,
),
TextButton(
onPressed: () => collapsed.value = !collapsed.value,
child: Text(
- collapsed.value ? 'Show More' : 'Show Less',
+ collapsed.value ? l.showMore : l.showLess,
),
),
],
diff --git a/lib/widgets/settings/option_container.dart b/lib/widgets/settings/option_container.dart
index c4d666bf..dab3cc34 100644
--- a/lib/widgets/settings/option_container.dart
+++ b/lib/widgets/settings/option_container.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
+import 'package:kover/utils/constants/kover_icons.dart';
import 'package:kover/utils/layout_constants.dart';
class OptionContainer extends StatelessWidget {
@@ -51,7 +52,7 @@ class OptionContainer extends StatelessWidget {
),
child: const IconButton(
icon: Icon(
- Icons.info_outline,
+ KoverIcons.info,
),
onPressed: null,
),
diff --git a/lib/widgets/util/login_guard.dart b/lib/widgets/util/login_guard.dart
index 253eb619..ef94eb89 100644
--- a/lib/widgets/util/login_guard.dart
+++ b/lib/widgets/util/login_guard.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/riverpod/providers/auth.dart';
import 'package:kover/riverpod/providers/router.dart';
import 'package:kover/riverpod/providers/settings/credentials.dart';
@@ -13,6 +14,7 @@ class LoginGuard extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final loginStatus = ref.watch(loginStatusProvider);
return switch (loginStatus) {
@@ -30,26 +32,26 @@ class LoginGuard extends ConsumerWidget {
),
const SizedBox(height: LayoutConstants.smallPadding),
Text(
- 'Not Signed In',
+ l.notSignedIn,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: LayoutConstants.smallerPadding),
- const Padding(
- padding: EdgeInsets.symmetric(
+ Padding(
+ padding: const EdgeInsets.symmetric(
horizontal: LayoutConstants.mediumPadding,
),
child: Text(
- 'No credentials configured. Please add your server URL and API key in Settings.',
+ l.noCredentialsDescription,
textAlign: TextAlign.center,
),
),
const SizedBox(height: LayoutConstants.smallPadding),
FilledButton(
onPressed: () => const SettingsRoute().go(context),
- child: const Text('Open Settings'),
+ child: Text(l.openSettings),
),
],
),
@@ -67,19 +69,19 @@ class LoginGuard extends ConsumerWidget {
),
const SizedBox(height: LayoutConstants.smallPadding),
Text(
- 'Connection Error',
+ l.connectionError,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: LayoutConstants.smallerPadding),
- const Padding(
- padding: EdgeInsets.symmetric(
+ Padding(
+ padding: const EdgeInsets.symmetric(
horizontal: LayoutConstants.mediumPadding,
),
child: Text(
- 'Failed to fetch user. Please check your credentials or try again.',
+ l.connectionErrorDescription,
textAlign: TextAlign.center,
),
),
@@ -94,7 +96,7 @@ class LoginGuard extends ConsumerWidget {
size: LayoutConstants.smallIcon,
color: Theme.of(context).colorScheme.onPrimary,
),
- label: const Text('Retry'),
+ label: Text(l.retry),
),
const SizedBox(height: LayoutConstants.smallPadding),
FilledButton.icon(
@@ -104,7 +106,7 @@ class LoginGuard extends ConsumerWidget {
size: LayoutConstants.smallIcon,
color: Theme.of(context).colorScheme.onPrimary,
),
- label: const Text('Open Settings'),
+ label: Text(l.openSettings),
),
],
),
diff --git a/lib/widgets/util/monitoring_opt_out_popup.dart b/lib/widgets/util/monitoring_opt_out_popup.dart
index b850d2bb..7605fbb8 100644
--- a/lib/widgets/util/monitoring_opt_out_popup.dart
+++ b/lib/widgets/util/monitoring_opt_out_popup.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/riverpod/providers/settings/general_settings.dart';
import 'package:kover/utils/layout_constants.dart';
@@ -8,20 +9,16 @@ class MonitoringOptOutPopup extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
return AlertDialog(
- title: const Text('Send anonymous crash reports and diagnostics?'),
- content: const Column(
+ title: Text(l.sendDiagnosticsDialogTitle),
+ content: Column(
spacing: LayoutConstants.mediumPadding,
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [
- Text(
- 'Help improve the app by sending anonymous error and '
- 'performance statistics. The data does not contain any '
- 'personal information and is uniquely used to improve '
- 'the app.',
- ),
- Text('This can be changed in the settings at any time.'),
+ Text(l.sendDiagnosticsDescription),
+ Text(l.sendDiagnosticsChangeable),
],
),
actions: [
@@ -40,14 +37,14 @@ class MonitoringOptOutPopup extends ConsumerWidget {
context,
).colorScheme.onError,
),
- child: const Text('No, thanks'),
+ child: Text(l.noThanks),
),
FilledButton(
onPressed: () {
ref.read(generalSettingsProvider.notifier).setSendDiagnostics(true);
Navigator.of(context).pop();
},
- child: const Text('I\'m in!'),
+ child: Text(l.imIn),
),
],
);
diff --git a/lib/widgets/util/navigator_container.dart b/lib/widgets/util/navigator_container.dart
index c24c3f50..c6a2f0aa 100644
--- a/lib/widgets/util/navigator_container.dart
+++ b/lib/widgets/util/navigator_container.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:kover/generated/l10n/app_localizations.dart';
import 'package:kover/riverpod/providers/settings/oneoffs.dart';
import 'package:kover/riverpod/providers/theme.dart' hide Theme;
import 'package:kover/utils/layout_constants.dart';
@@ -16,6 +17,7 @@ class NavigatorContainer extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final l = AppLocalizations.of(context);
final oneOffs = ref.watch(oneOffsProvider);
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -69,18 +71,18 @@ class NavigatorContainer extends ConsumerWidget {
initialLocation: true,
);
},
- destinations: const [
+ destinations: [
NavigationDestination(
- icon: Icon(LucideIcons.house),
- label: 'Home',
+ icon: const Icon(LucideIcons.house),
+ label: l.home,
),
NavigationDestination(
- icon: Icon(LucideIcons.star),
- label: 'Want to Read',
+ icon: const Icon(LucideIcons.star),
+ label: l.wantToRead,
),
NavigationDestination(
- icon: Icon(LucideIcons.library),
- label: 'Menu',
+ icon: const Icon(LucideIcons.library),
+ label: l.menu,
),
],
),
diff --git a/pubspec.lock b/pubspec.lock
index d4f5873d..6ad7e1ab 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -438,6 +438,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
+ flutter_localizations:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
flutter_markdown_plus:
dependency: "direct main"
description:
@@ -720,6 +725,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
+ intl:
+ dependency: "direct main"
+ description:
+ name: intl
+ sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.20.2"
io:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 87ab3211..c3835a98 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,21 +1,7 @@
name: kover
description: "An unofficial cross-platform Kavita frontend."
-# The following line prevents the package from being accidentally published to
-# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: "none" # Remove this line if you wish to publish to pub.dev
-# The following defines the version and build number for your application.
-# A version number is three numbers separated by dots, like 1.2.43
-# followed by an optional build number separated by a +.
-# Both the version and the builder number may be overridden in flutter
-# build by specifying --build-name and --build-number, respectively.
-# In Android, build-name is used as versionName while build-number used as versionCode.
-# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
-# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
-# Read more about iOS versioning at
-# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-# In Windows, build-name is used as the major, minor, and patch parts
-# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
@@ -59,6 +45,9 @@ dependencies:
flutter_secure_storage: ^10.2.0
package_info_plus: ^10.1.0
url_launcher: ^6.3.2
+ flutter_localizations:
+ sdk: flutter
+ intl: ^0.20.0
dev_dependencies:
sentry_dart_plugin: ^3.4.0
@@ -76,45 +65,12 @@ dev_dependencies:
go_router_builder: ^4.2.0
yaml: ^3.1.3
-# For information on the generic Dart part of this file, see the
-# following page: https://dart.dev/tools/pub/pubspec
-
-# The following section is specific to Flutter packages.
flutter:
- # The following line ensures that the Material Icons font is
- # included with your application, so that you can use the icons in
- # the material Icons class.
+ generate: true
uses-material-design: true
-
assets:
- assets/icon/icon.png
- # An image asset can refer to one or more resolution-specific "variants", see
- # https://flutter.dev/to/resolution-aware-images
-
- # For details regarding adding assets from package dependencies, see
- # https://flutter.dev/to/asset-from-package
-
- # To add custom fonts to your application, add a fonts section here,
- # in this "flutter" section. Each entry in this list should have a
- # "family" key with the font family name, and a "fonts" key with a
- # list giving the asset and other descriptors for the font. For
- # example:
- # fonts:
- # - family: Schyler
- # fonts:
- # - asset: fonts/Schyler-Regular.ttf
- # - asset: fonts/Schyler-Italic.ttf
- # style: italic
- # - family: Trajan Pro
- # fonts:
- # - asset: fonts/TrajanPro.ttf
- # - asset: fonts/TrajanPro_Bold.ttf
- # weight: 700
- #
- # For details regarding fonts from package dependencies,
- # see https://flutter.dev/to/font-from-package
-
sentry:
upload_debug_symbols: true
upload_source_maps: true