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 +[![CI](https://github.com/rodonisi/kover/actions/workflows/ci.yaml/badge.svg)](https://github.com/rodonisi/kover/actions/workflows/ci.yaml) +[![Build & Deploy](https://github.com/rodonisi/kover/actions/workflows/build-and-deploy.yml/badge.svg)](https://github.com/rodonisi/kover/actions/workflows/build-and-deploy.yml) +Translation status +![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/rodonisi/kover/total) + An unofficial cross-platform [Kavita](https://www.kavitareader.com/) client. @@ -114,3 +119,17 @@ To connect Kover to a Kavita instance: Screenshot Screenshot

+ +## Thanks + +

+ + Sentry + + + Weblate + +

+ +- [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