diff --git a/apps/mobile/src/lib/__fixtures__/shared-catalog.golden.json b/apps/mobile/src/lib/__fixtures__/shared-catalog.golden.json index d1b3ba90..1d47242d 100644 --- a/apps/mobile/src/lib/__fixtures__/shared-catalog.golden.json +++ b/apps/mobile/src/lib/__fixtures__/shared-catalog.golden.json @@ -21,13 +21,7 @@ "about.techStackLabel": "Stack", "about.techStackValue": "ASP.NET Core 10 · PostgreSQL 16 · React 19 · React Native (Expo) · Ollama (Gemma 4) · OpenAI gpt-4.1-nano · Docker · Cloudflare Tunnel", "about.title": "About", - "addMenu.ariaLabel": "Add content menu", "addMenu.browseAll": "Browse all books", - "addMenu.browserExtension": "Browser extension", - "addMenu.comingSoon": "Coming in a future update", - "addMenu.emailBook": "Email a book", - "addMenu.mobileApps": "Mobile apps", - "addMenu.pasteUrl": "Paste URL", "addMenu.title": "Add to library", "addMenu.uploadFile": "Upload file", "authors.bioAndBooks": "{name} - Biography and books", @@ -45,7 +39,6 @@ "bookDetail.backToBooks": "Back to Books", "bookDetail.backToHome": "Back to Home", "bookDetail.browseAll": "Browse all books", - "bookDetail.chaptersHeading": "Chapters", "bookDetail.continueReading": "Continue Reading", "bookDetail.downloadForOffline": "Download for offline", "bookDetail.downloading": "Downloading {progress}%...", @@ -133,11 +126,8 @@ "guest.signOutTitle": "Sign out and lose everything?", "highlights.emptySubtitle": "Select text while reading to highlight it", "highlights.emptyTitle": "No highlights yet", - "home.hero.cta": "Browse catalog", - "home.hero.subtitle": "Read classic literature. Build vocabulary. Track your progress.", "home.recentAuthors.title": "Recently Added Authors", "home.recentAuthors.viewAll": "View all authors", - "home.recentBooks.read": "Read", "home.recentBooks.title": "Recently Added Books", "home.recentBooks.viewAll": "View all books", "librarian.ask": "Ask", @@ -174,12 +164,10 @@ "library.actions.confirmDeleteTitle": "Delete this book?", "library.actions.delete": "Delete", "library.actions.deleting": "Deleting…", - "library.actions.downloadOriginal": "Download original", "library.actions.editMetadata": "Edit metadata", "library.actions.markFinished": "Mark as finished", "library.actions.markUnfinished": "Mark as unfinished", "library.actions.menu": "Options", - "library.actions.open": "Open", "library.actions.removeFromLibrary": "Remove from library", "library.actions.retry": "Re-process", "library.actions.retrying": "Retrying…", @@ -201,14 +189,9 @@ "library.discuss.label": "Talk this book over", "library.emptyLibrary": "Your library is empty.", "library.failed": "Failed", - "library.failedToProcess": "Failed to process", - "library.filter.all": "All", "library.filter.clear": "Clear filter", "library.filter.empty": "No books match this filter", - "library.filter.failed": "Failed", "library.filter.finished": "Finished", - "library.filter.notStarted": "Not started", - "library.filter.reading": "Reading", "library.firstBook.browse": "Or browse free classics", "library.firstBook.copy": "Add a book you already want to finish. Tap any word while you read to translate it and save it to your vocabulary.", "library.firstBook.guestBrowse": "Browse free books", @@ -223,7 +206,6 @@ "library.loadFailed.body": "Something went wrong on our side. Your books are safe.", "library.loadFailed.title": "Couldn't load your library", "library.loading": "Loading...", - "library.noUploads": "No uploaded books yet.", "library.offline.body": "Your library will be here as soon as you reconnect.", "library.offline.partial": "You're offline — showing downloaded books only.", "library.offline.title": "You're offline", @@ -234,11 +216,9 @@ "library.resume.continue": "Continue", "library.resume.percentComplete": "{percent}% complete", "library.resume.start": "Start reading", - "library.saved": "Saved", "library.search.clear": "Clear search", "library.search.empty": "No books match \"{query}\"", "library.search.placeholder": "Search your library…", - "library.search.shortcut": "Cmd+F to focus", "library.shelves.continueReading.subtitle": "Pick up where you left off", "library.shelves.continueReading.title": "Continue reading", "library.shelves.empty.browseCatalog": "Browse catalog", @@ -266,17 +246,8 @@ "library.sort.progress": "Most read", "library.sort.recent": "Last read", "library.sort.title": "Title (A→Z)", - "library.sortBy": "Sort by", - "library.sortProgress": "Progress", - "library.sortRecent": "Recently Added", - "library.sortTitle": "Title", "library.status.all": "All", "library.status.ariaLabel": "Filter by status", - "library.status.empty.all": "No books match this filter", - "library.status.empty.failed": "These uploads need attention", - "library.status.empty.finished": "Finish your first book this month", - "library.status.empty.notStarted": "Looking good — you're on top of your library", - "library.status.empty.reading": "Open a book to start reading", "library.status.failed": "Failed", "library.status.finished": "Finished", "library.status.notStarted": "Not started", @@ -289,8 +260,6 @@ "library.timeMinAgo": "min ago", "library.timeYesterday": "yesterday", "library.title": "My Library", - "library.uploadHint": "Click the + button to upload EPUB or PDF files.", - "library.uploads": "Uploads", "library.view.done": "Done", "library.view.grid": "Grid", "library.view.layout": "Layout", @@ -301,27 +270,13 @@ "library.view.source": "Show", "library.view.title": "View", "nav.about": "About", - "nav.browseBooks": "Browse all books", "nav.catalog": "Catalog", "nav.discover": "Discover", "nav.library": "Library", "nav.profile": "Profile", - "nav.read": "Read", - "nav.stats": "Stats", "nav.vocabulary": "Vocabulary", - "onboarding.chooseLanguage": "Choose your language", - "onboarding.chooseLanguageSubtitle": "You can change this later in settings", - "onboarding.gotIt": "Got it", - "onboarding.learnByReading": "Learn by reading", "onboarding.nativeLanguageSubtitle": "We'll translate into it while you read. You can change it any time in Profile.", "onboarding.nativeLanguageTitle": "What language do you know best?", - "onboarding.next": "Next", - "onboarding.ready": "You're ready!", - "onboarding.readySubtitle": "Saved words appear in your vocabulary\nfor spaced repetition review", - "onboarding.save": "Save", - "onboarding.saveWords": "Save words to build your vocabulary", - "onboarding.skip": "Skip", - "onboarding.tapToTranslate": "Tap any word to see its translation", "privacy.aiBody1": "They are optional. If you do not use Book Chat, the tutor, the librarian, Explain or translation, nothing is sent to OpenAI.", "privacy.aiBody2": "AI output can be wrong. Explanations, translations and answers about a book are generated by a language model and should not be relied on as fact.", "privacy.aiBody3": "We do not train models on your content, and under OpenAI's API terms neither do they. We do keep a sample of prompts and responses, as described above, for quality and cost monitoring.", @@ -387,7 +342,6 @@ "profile.vocabReminder.permissionDeniedTitle": "Notifications off", "profile.vocabReminder.unavailableBody": "Reminders aren't available in this build. Install the latest app version to enable them.", "profile.vocabReminder.unavailableTitle": "Not available", - "reader.ask.askAboutThis": "Ask about this", "reader.ask.clearChat": "Clear chat", "reader.ask.clearConfirm": "Clear this chat? This can't be undone.", "reader.ask.detachQuote": "Remove quoted passage", @@ -415,7 +369,6 @@ "reader.coachmarkDismiss": "Got it", "reader.coachmarkSubtitle": "Press and hold a word to see its translation, then tap Save to add it to your vocabulary. Tap the page to show or hide the toolbars.", "reader.coachmarkTitle": "Press and hold a word", - "reader.later": "Later", "reader.ownBookAsk.body": "Long-press, translate, save, review — it all works the same on an EPUB or PDF of your own. Those are the books you'll actually finish.", "reader.ownBookAsk.cta": "Add my own book", "reader.ownBookAsk.dismiss": "Not now", @@ -424,11 +377,8 @@ "reader.rareWordNotice.cta": "Add to SRS anyway", "reader.rareWordNotice.ctaBusy": "Adding…", "reader.rareWordNotice.title": "Rare word — saved to reference", - "reader.reviewNow": "Review Now", "reader.timeLeft.hours": "{hours} hr left", "reader.timeLeft.hoursMinutes": "{hours} hr {minutes} min left", - "reader.timeLeft.inBook": "left in book", - "reader.timeLeft.inChapter": "left in chapter", "reader.timeLeft.minutes": "{minutes} min left", "reader.timeLeft.under": "Less than a minute left", "reader.toastTapToReview": "Tap to review", @@ -438,18 +388,11 @@ "reader.vocab.addedToSrs": "Added to your vocabulary", "reader.vocab.queuedForTomorrow": "Daily cap reached — queued for tomorrow", "reader.vocab.saveNeedsAccount": "Saving words needs an account — this one wasn't kept. Sign in from Profile.", - "reader.vocab.savedToReference": "Saved to reference (rare word)", "reader.vocab.tapAgainToStudy": "Tap again to study ({n} left)", - "reader.wordsSaved": "{count} word(s) saved", - "review.dayStreak": "{count} day streak", - "review.excellent": "Excellent!", - "review.greatWork": "Great work!", - "review.keepPracticing": "Keep practicing!", "search.chapter": "Chapter", "search.clearAll": "Clear all", "search.enterQuery": "Enter a search query", "search.foundEditions": "Found {total} books", - "search.foundResults": "Found {total} results", "search.hideChapters": "Hide chapters", "search.moreChapters": "+{count} more chapters", "search.next": "Next →", @@ -467,7 +410,6 @@ "stats.achievements": "Achievements", "stats.allTime": "All Time", "stats.authors": "Most Read Authors", - "stats.avgDaily": "Avg Daily", "stats.avgTimeToFinish": "Avg Days to Finish", "stats.avgWpm": "Avg Words/Min", "stats.bookLength": "Book Length", @@ -482,15 +424,11 @@ "stats.details": "Details", "stats.emptySubtitle": "Start reading a book to track your progress", "stats.emptyTitle": "No reading stats yet", - "stats.fast": "Fast", "stats.genres": "Genres", "stats.goalMet": "Goal met!", - "stats.goalSettings": "Goal Settings", "stats.hours": "Hours", "stats.languages": "Languages", - "stats.long": "Long", "stats.longestStreak": "Longest Streak", - "stats.medium": "Medium", "stats.noBookStats": "No book stats yet", "stats.noData": "No data yet", "stats.pace": "Pace", @@ -498,10 +436,7 @@ "stats.readingTimeByAuthor": "Reading Time by Author", "stats.readingTimeByGenre": "Reading Time by Genre", "stats.saveGoal": "Save", - "stats.short": "Short", "stats.signInPrompt": "Sign in to track your reading and unlock achievements.", - "stats.slow": "Slow", - "stats.streakCalendar": "Reading Activity", "stats.streakThreshold": "Min minutes for streak", "stats.streaks": "Streaks", "stats.summary": "Summary", @@ -572,47 +507,26 @@ "tutor.summary.done": "Session complete", "tutor.summary.studied": "Studied", "tutor.title": "Smart session", - "vocabulary.alreadySaved": "Already saved", "vocabulary.banner.budgetReached": "Weekly goal reached. See you next week!", "vocabulary.clusters.bonusCta": "Bonus: {n} related words", "vocabulary.clusters.fromBook": "from {book}", "vocabulary.clusters.start": "Start bonus", - "vocabulary.definition": "Definition", "vocabulary.dueDate": "Due", - "vocabulary.dueToday": "Due today", "vocabulary.editTranslation": "Edit translation", "vocabulary.empty": "No words saved yet. Select words while reading to add them.", - "vocabulary.filters.all": "All", - "vocabulary.filters.learning": "Learning", - "vocabulary.filters.mastered": "Mastered", - "vocabulary.filters.new": "New", "vocabulary.filters.search": "Search words...", - "vocabulary.learning": "Learning", "vocabulary.mastered": "Mastered", - "vocabulary.noReviewDue": "No words due for review!", "vocabulary.offline.body": "Your saved words will be here as soon as you reconnect.", "vocabulary.practice.cta": "Practice anyway", - "vocabulary.practice.subtitle": "Doesn't affect your SRS schedule", "vocabulary.practice.title": "Practice", - "vocabulary.retired.subtitle": "Graduated from review. Tap to bring back.", - "vocabulary.retired.title": "Mastered — retired", - "vocabulary.retired.unretire": "Bring back to review", "vocabulary.review.backToVocab": "Back to Vocabulary", - "vocabulary.review.check": "Check", - "vocabulary.review.chooseTranslation": "Choose the correct translation", - "vocabulary.review.closeEnough": "Close enough!", "vocabulary.review.correct": "Correct!", "vocabulary.review.correctAnswer": "Correct answer", "vocabulary.review.correctRate": "Correct rate", - "vocabulary.review.fillBlank": "What word fills the blank?", "vocabulary.review.fromBook": "from \"{title}\"", "vocabulary.review.next": "Next", - "vocabulary.review.promoted": "Level up!", "vocabulary.review.reviewed": "Reviewed", - "vocabulary.review.sessionComplete": "Session complete!", - "vocabulary.review.title": "Vocabulary Review", "vocabulary.review.wrong": "Not quite", - "vocabulary.saveWord": "Save word", "vocabulary.settings.autoRetire": "Auto-retire mastered words", "vocabulary.settings.autoRetireHint": "Stop reviewing words you've answered correctly 3× at long intervals.", "vocabulary.settings.autoSpeak": "Say the word on a card", @@ -634,25 +548,11 @@ "vocabulary.sort.due": "Due date", "vocabulary.sort.recent": "Recent", "vocabulary.sort.stage": "Stage", - "vocabulary.source": "Source", - "vocabulary.stage": "Stage", - "vocabulary.stages.0": "New", - "vocabulary.stages.1": "Recognition", - "vocabulary.stages.2": "Recall", - "vocabulary.stages.3": "Context", - "vocabulary.stages.4": "Mastered", - "vocabulary.startReview": "Start Review", - "vocabulary.stats.correctRate": "Correct rate", - "vocabulary.stats.reviewedToday": "Reviewed today", "vocabulary.stats.streak": "Review streak", "vocabulary.title": "Vocabulary", "vocabulary.totalWords": "Total words", - "vocabulary.translation": "Translation", "vocabulary.weeklyBudget.backToReading": "Back to reading", "vocabulary.weeklyBudget.emptyStateSubtitle": "You've hit your weekly review target. Read — new reviews unlock as old ones age out.", "vocabulary.weeklyBudget.label": "This week", - "vocabulary.weeklyBudget.progress": "{used} / {budget}", - "vocabulary.weeklyBudget.remaining": "{n} left this week", - "vocabulary.word": "Word", - "vocabulary.wordSaved": "Word saved" + "vocabulary.weeklyBudget.progress": "{used} / {budget}" } diff --git a/apps/web/src/locales/__tests__/__fixtures__/web-catalog.golden.json b/apps/web/src/locales/__tests__/__fixtures__/web-catalog.golden.json index ef1f9c1d..cae56992 100644 --- a/apps/web/src/locales/__tests__/__fixtures__/web-catalog.golden.json +++ b/apps/web/src/locales/__tests__/__fixtures__/web-catalog.golden.json @@ -21,13 +21,7 @@ "about.techStackLabel": "Stack", "about.techStackValue": "ASP.NET Core 10 · PostgreSQL 16 · React 19 · React Native (Expo) · Ollama (Gemma 4) · OpenAI gpt-4.1-nano · Docker · Cloudflare Tunnel", "about.title": "About", - "addMenu.ariaLabel": "Add content menu", "addMenu.browseAll": "Browse all books", - "addMenu.browserExtension": "Browser extension", - "addMenu.comingSoon": "Coming in a future update", - "addMenu.emailBook": "Email a book", - "addMenu.mobileApps": "Mobile apps", - "addMenu.pasteUrl": "Paste URL", "addMenu.title": "Add to library", "addMenu.uploadFile": "Upload file", "androidBeta.badgeAction": "Join the beta", @@ -54,7 +48,6 @@ "bookDetail.backToBooks": "Back to Books", "bookDetail.backToHome": "Back to Home", "bookDetail.browseAll": "Browse all books", - "bookDetail.chaptersHeading": "Chapters", "bookDetail.continueReading": "Continue Reading", "bookDetail.downloadEpub": "Download EPUB", "bookDetail.downloadForOffline": "Download for offline", @@ -338,7 +331,6 @@ "home.features.upload.description": "Upload EPUB or PDF. Read your books with the full TextStack Reader toolkit.", "home.features.upload.title": "Your own books", "home.hero.brandLine": "TextStack — a reader for developers, students, and curious minds.", - "home.hero.cta": "Browse catalog", "home.hero.ctaPrimary": "Start reading", "home.hero.ctaReturning": "Continue where you left off", "home.hero.ctaSecondary": "Upload your book", @@ -346,7 +338,6 @@ "home.hero.searchPlaceholder": "Search by title, author, or genre...", "home.hero.seoDescription": "Finish the tech book, paper, or classic you keep quitting. Contextual word explanations tied to the book's domain. Capped weekly SRS, offline reading. EPUB, PDF.", "home.hero.seoTitle": "TextStack — A reader that helps you finish dense books", - "home.hero.subtitle": "Read classic literature. Build vocabulary. Track your progress.", "home.hero.subtitleAfter": "— stay in flow, finish the book.", "home.hero.subtitleBefore": "Tap any word for a contextual explanation in", "home.hero.title": "Finish the book you keep quitting.", @@ -360,7 +351,6 @@ "home.howItWorks.step3Title": "Master with flashcards", "home.recentAuthors.title": "Recently Added Authors", "home.recentAuthors.viewAll": "View all authors", - "home.recentBooks.read": "Read", "home.recentBooks.title": "Recently Added Books", "home.recentBooks.viewAll": "View all books", "home.recommended.subtitle": "Free, public-domain books — thousands ready to read.", @@ -423,12 +413,10 @@ "library.actions.confirmDeleteTitle": "Delete this book?", "library.actions.delete": "Delete", "library.actions.deleting": "Deleting…", - "library.actions.downloadOriginal": "Download original", "library.actions.editMetadata": "Edit metadata", "library.actions.markFinished": "Mark as finished", "library.actions.markUnfinished": "Mark as unfinished", "library.actions.menu": "Options", - "library.actions.open": "Open", "library.actions.removeFromLibrary": "Remove from library", "library.actions.retry": "Re-process", "library.actions.retrying": "Retrying…", @@ -497,14 +485,9 @@ "library.estimate.fromYourPace": "Based on your reading pace", "library.estimate.left": "{{time}} left", "library.failed": "Failed", - "library.failedToProcess": "Failed to process", - "library.filter.all": "All", "library.filter.clear": "Clear filter", "library.filter.empty": "No books match this filter", - "library.filter.failed": "Failed", "library.filter.finished": "Finished", - "library.filter.notStarted": "Not started", - "library.filter.reading": "Reading", "library.firstBook.browse": "Or browse free classics", "library.firstBook.copy": "Add a book you already want to finish. Tap any word while you read to translate it and save it to your vocabulary.", "library.firstBook.guestBrowse": "Browse free books", @@ -519,7 +502,6 @@ "library.loadFailed.body": "Something went wrong on our side. Your books are safe.", "library.loadFailed.title": "Couldn't load your library", "library.loading": "Loading...", - "library.noUploads": "No uploaded books yet.", "library.offline.body": "Your library will be here as soon as you reconnect.", "library.offline.partial": "You're offline — showing downloaded books only.", "library.offline.title": "You're offline", @@ -563,12 +545,10 @@ "library.resume.continue": "Continue", "library.resume.percentComplete": "{percent}% complete", "library.resume.start": "Start reading", - "library.saved": "Saved", "library.search.clear": "Clear search", "library.search.contentToggle": "Search inside books", "library.search.empty": "No books match \"{query}\"", "library.search.placeholder": "Search your library…", - "library.search.shortcut": "Cmd+F to focus", "library.shelves.continueReading.subtitle": "Pick up where you left off", "library.shelves.continueReading.title": "Continue reading", "library.shelves.empty.browseCatalog": "Browse catalog", @@ -597,10 +577,6 @@ "library.sort.progress": "Progress (most read)", "library.sort.recent": "Recently opened", "library.sort.title": "Title (A→Z)", - "library.sortBy": "Sort by", - "library.sortProgress": "Progress", - "library.sortRecent": "Recently Added", - "library.sortTitle": "Title", "library.statsHeader.booksSuffix": "books", "library.statsHeader.booksYtd": "Books finished YTD", "library.statsHeader.collapse": "Hide stats", @@ -619,11 +595,6 @@ "library.statsHeader.yearlyGoal": "Year's goal", "library.status.all": "All", "library.status.ariaLabel": "Filter by status", - "library.status.empty.all": "No books match this filter", - "library.status.empty.failed": "These uploads need attention", - "library.status.empty.finished": "Finish your first book this month", - "library.status.empty.notStarted": "Looking good — you're on top of your library", - "library.status.empty.reading": "Open a book to start reading", "library.status.failed": "Failed", "library.status.finished": "Finished", "library.status.notStarted": "Not started", @@ -647,8 +618,6 @@ "library.timeMinAgo": "min ago", "library.timeYesterday": "yesterday", "library.title": "My Library", - "library.uploadHint": "Click the + button to upload EPUB or PDF files.", - "library.uploads": "Uploads", "library.view.done": "Done", "library.view.grid": "Grid", "library.view.layout": "Layout", @@ -704,7 +673,6 @@ "nav.aboutTextStack": "About TextStack Reader", "nav.books": "Books", "nav.brandTitle": "TextStack Reader - Learn languages through reading", - "nav.browseBooks": "Browse all books", "nav.browseGenres": "Browse genres", "nav.catalog": "Catalog", "nav.discover": "Discover", @@ -721,25 +689,12 @@ "nav.highlights": "Highlights", "nav.library": "Library", "nav.profile": "Profile", - "nav.read": "Read", "nav.search": "Search", - "nav.stats": "Stats", "nav.vocabulary": "Vocabulary", - "onboarding.chooseLanguage": "Choose your language", - "onboarding.chooseLanguageSubtitle": "You can change this later in settings", "onboarding.ghostHintLabel": "Tap any word", - "onboarding.gotIt": "Got it", "onboarding.hintLabel": "Try tapping a word", - "onboarding.learnByReading": "Learn by reading", "onboarding.nativeLanguageSubtitle": "We'll translate into it while you read. You can change it any time in Profile.", "onboarding.nativeLanguageTitle": "What language do you know best?", - "onboarding.next": "Next", - "onboarding.ready": "You're ready!", - "onboarding.readySubtitle": "Saved words appear in your vocabulary\nfor spaced repetition review", - "onboarding.save": "Save", - "onboarding.saveWords": "Save words to build your vocabulary", - "onboarding.skip": "Skip", - "onboarding.tapToTranslate": "Tap any word to see its translation", "palette.empty": "Nothing matches that.", "palette.groups.actions": "Actions", "palette.groups.books": "Books", @@ -826,7 +781,6 @@ "profile.vocabReminder.permissionDeniedTitle": "Notifications off", "profile.vocabReminder.unavailableBody": "Reminders aren't available in this build. Install the latest app version to enable them.", "profile.vocabReminder.unavailableTitle": "Not available", - "reader.ask.askAboutThis": "Ask about this", "reader.ask.citation": "ch.{{ch}}", "reader.ask.citationFallback": "source", "reader.ask.citationPage": "p. {{page}}", @@ -872,7 +826,6 @@ "reader.image.reset": "Reset zoom", "reader.image.zoomIn": "Zoom in", "reader.image.zoomOut": "Zoom out", - "reader.later": "Later", "reader.noteEditor.cancel": "Cancel", "reader.noteEditor.close": "Close", "reader.noteEditor.deleteHighlight": "Delete highlight", @@ -900,7 +853,6 @@ "reader.rareWordNotice.cta": "Add to SRS anyway", "reader.rareWordNotice.ctaBusy": "Adding…", "reader.rareWordNotice.title": "Rare word — saved to reference", - "reader.reviewNow": "Review Now", "reader.selectionToolbar.askAboutThis": "Ask about this", "reader.selectionToolbar.copy": "Copy selected text", "reader.selectionToolbar.explain": "Explain in context", @@ -919,8 +871,6 @@ "reader.studyBuddy.title": "Help me understand this", "reader.timeLeft.hours": "{hours} hr left", "reader.timeLeft.hoursMinutes": "{hours} hr {minutes} min left", - "reader.timeLeft.inBook": "left in book", - "reader.timeLeft.inChapter": "left in chapter", "reader.timeLeft.minutes": "{minutes} min left", "reader.timeLeft.under": "Less than a minute left", "reader.toastTapToReview": "Tap to review", @@ -932,7 +882,6 @@ "reader.vocab.addedToSrs": "Added to your vocabulary", "reader.vocab.queuedForTomorrow": "Daily cap reached — queued for tomorrow", "reader.vocab.saveNeedsAccount": "Saving words needs an account — this one wasn't kept. Sign in from Profile.", - "reader.vocab.savedToReference": "Saved to reference (rare word)", "reader.vocab.tapAgainToStudy": "Tap again to study ({{n}} left)", "reader.wordPopup.allLanguages": "All languages", "reader.wordPopup.change": "Change", @@ -953,16 +902,10 @@ "reader.wordPopup.searchLanguage": "Search language...", "reader.wordPopup.translateTo": "Translate to...", "reader.wordPopup.translatingTo": "Translating to", - "reader.wordsSaved": "{count} word(s) saved", - "review.dayStreak": "{count} day streak", - "review.excellent": "Excellent!", - "review.greatWork": "Great work!", - "review.keepPracticing": "Keep practicing!", "search.chapter": "Chapter", "search.clearAll": "Clear all", "search.enterQuery": "Enter a search query", "search.foundEditions": "Found {total} books", - "search.foundResults": "Found {total} results", "search.hideChapters": "Hide chapters", "search.moreChapters": "+{count} more chapters", "search.next": "Next →", @@ -996,7 +939,6 @@ "stats.achievements": "Achievements", "stats.allTime": "All Time", "stats.authors": "Most Read Authors", - "stats.avgDaily": "Avg Daily", "stats.avgTimeToFinish": "Avg Days to Finish", "stats.avgWpm": "Avg Words/Min", "stats.bookLength": "Book Length", @@ -1017,15 +959,11 @@ "stats.empty.title": "Your reading stats will appear here", "stats.emptySubtitle": "Start reading a book to track your progress", "stats.emptyTitle": "No reading stats yet", - "stats.fast": "Fast", "stats.genres": "Genres", "stats.goalMet": "Goal met!", - "stats.goalSettings": "Goal Settings", "stats.hours": "Hours", "stats.languages": "Languages", - "stats.long": "Long", "stats.longestStreak": "Longest Streak", - "stats.medium": "Medium", "stats.noBookStats": "No book stats yet", "stats.noData": "No data yet", "stats.overview": "Overview", @@ -1034,10 +972,7 @@ "stats.readingTimeByAuthor": "Reading Time by Author", "stats.readingTimeByGenre": "Reading Time by Genre", "stats.saveGoal": "Save", - "stats.short": "Short", "stats.signInPrompt": "Sign in to track your reading and unlock achievements.", - "stats.slow": "Slow", - "stats.streakCalendar": "Reading Activity", "stats.streakThreshold": "Min minutes for streak", "stats.streaks": "Streaks", "stats.summary": "Summary", @@ -1145,7 +1080,6 @@ "userBook.enrichRetry": "Retry", "userBook.enriching": "Generating book details…", "userMenu.anonymousReader": "Anonymous reader", - "vocabulary.alreadySaved": "Already saved", "vocabulary.banner.budgetReached": "Weekly goal reached. See you next week!", "vocabulary.banner.dayStreak": "day streak", "vocabulary.banner.dueWaiting": "{n} words waiting for review", @@ -1168,7 +1102,6 @@ "vocabulary.columns.progress": "Progress", "vocabulary.columns.translation": "Translation", "vocabulary.columns.word": "Word", - "vocabulary.definition": "Definition", "vocabulary.deleteAll": "Delete all words", "vocabulary.deleteAllConfirm": "Click to confirm — cannot be undone", "vocabulary.deleteConfirm": "Remove", @@ -1180,22 +1113,16 @@ "vocabulary.due.today": "Today", "vocabulary.due.tomorrow": "Tomorrow", "vocabulary.dueDate": "Due", - "vocabulary.dueToday": "Due today", "vocabulary.editTranslation": "Edit translation", "vocabulary.empty": "No words saved yet. Select words while reading to add them.", "vocabulary.emptyPage.cta": "Browse library", "vocabulary.emptyPage.subtitle": "Open any book, tap a word — it will appear here ready for review.", "vocabulary.emptyPage.title": "Your vocabulary starts here", - "vocabulary.filters.all": "All", - "vocabulary.filters.learning": "Learning", "vocabulary.filters.lookups": "Reference", - "vocabulary.filters.mastered": "Mastered", - "vocabulary.filters.new": "New", "vocabulary.filters.pending": "Pending", "vocabulary.filters.search": "Search words...", "vocabulary.fromBook": "from {title}", "vocabulary.interval": "Next interval: {n} days", - "vocabulary.learning": "Learning", "vocabulary.listCount": "{n} words", "vocabulary.listTitle": "My vocabulary", "vocabulary.loadMore": "Load more", @@ -1208,7 +1135,6 @@ "vocabulary.lookups.promoteFailed": "Could not promote this word. Try again.", "vocabulary.lookups.taps": "{n} taps", "vocabulary.mastered": "Mastered", - "vocabulary.noReviewDue": "No words due for review!", "vocabulary.offline.body": "Your saved words will be here as soon as you reconnect.", "vocabulary.pending.dailyStatus": "Today: {used}/{cap} new words in SRS", "vocabulary.pending.dismiss": "Dismiss", @@ -1222,26 +1148,18 @@ "vocabulary.practice.flashcards": "Flashcards", "vocabulary.practice.length": "Length", "vocabulary.practice.mode": "Mode", - "vocabulary.practice.subtitle": "Doesn't affect your SRS schedule", "vocabulary.practice.title": "Practice", "vocabulary.practice.wordsUnit": "words", - "vocabulary.retired.subtitle": "Graduated from review. Tap to bring back.", - "vocabulary.retired.title": "Mastered — retired", - "vocabulary.retired.unretire": "Bring back to review", "vocabulary.review.almost": "Almost", "vocabulary.review.backToVocab": "Back to Vocabulary", "vocabulary.review.blitz": "Blitz", - "vocabulary.review.check": "Check", - "vocabulary.review.chooseTranslation": "Choose the correct translation", "vocabulary.review.classic": "Classic", - "vocabulary.review.closeEnough": "Close enough!", "vocabulary.review.continue": "Continue", "vocabulary.review.correct": "Correct!", "vocabulary.review.correctAnswer": "Correct answer", "vocabulary.review.correctRate": "Correct rate", "vocabulary.review.didYouRemember": "Did you remember?", "vocabulary.review.excellent": "Excellent!", - "vocabulary.review.fillBlank": "What word fills the blank?", "vocabulary.review.forgot": "Forgot", "vocabulary.review.fromBook": "from \"{title}\"", "vocabulary.review.greatWork": "Great work!", @@ -1257,18 +1175,14 @@ "vocabulary.review.practiceAgain": "Practice Again", "vocabulary.review.practiceMode": "Practice Mode", "vocabulary.review.practiceNote": "Correct answers don't change your review schedule.", - "vocabulary.review.promoted": "Level up!", "vocabulary.review.reviewDue": "Review Due Words", "vocabulary.review.reviewed": "Reviewed", - "vocabulary.review.sessionComplete": "Session complete!", "vocabulary.review.soundOff": "Sound off", "vocabulary.review.soundOn": "Sound on", "vocabulary.review.startAgain": "Start Again", "vocabulary.review.tapToFlip": "Tap to flip", - "vocabulary.review.title": "Vocabulary Review", "vocabulary.review.wordsReviewed": "words", "vocabulary.review.wrong": "Not quite", - "vocabulary.saveWord": "Save word", "vocabulary.settings.autoRetire": "Auto-retire mastered words", "vocabulary.settings.autoRetireHint": "Stop reviewing words you've answered correctly 3× at long intervals.", "vocabulary.settings.autoSpeak": "Say the word on a card", @@ -1290,30 +1204,16 @@ "vocabulary.sort.due": "Due date", "vocabulary.sort.recent": "Recent", "vocabulary.sort.stage": "Stage", - "vocabulary.source": "Source", "vocabulary.srsHint": "Spaced repetition — each word returns when it's due for review.", - "vocabulary.stage": "Stage", - "vocabulary.stages.0": "New", - "vocabulary.stages.1": "Recognition", - "vocabulary.stages.2": "Recall", - "vocabulary.stages.3": "Context", - "vocabulary.stages.4": "Mastered", "vocabulary.startPractice": "Practice", - "vocabulary.startReview": "Start Review", - "vocabulary.stats.correctRate": "Correct rate", "vocabulary.stats.practicedToday": "Practiced today", - "vocabulary.stats.reviewedToday": "Reviewed today", "vocabulary.stats.streak": "Review streak", "vocabulary.title": "Vocabulary", "vocabulary.totalWords": "Total words", - "vocabulary.translation": "Translation", "vocabulary.weeklyBudget.backToReading": "Back to reading", "vocabulary.weeklyBudget.emptyStateSubtitle": "You've hit your weekly review target. Read — new reviews unlock as old ones age out.", "vocabulary.weeklyBudget.label": "This week", "vocabulary.weeklyBudget.progress": "{used} / {budget}", - "vocabulary.weeklyBudget.remaining": "{n} left this week", - "vocabulary.word": "Word", - "vocabulary.wordSaved": "Word saved", "words.tabs.highlights": "Highlights", "words.tabs.vocabulary": "Vocabulary" } diff --git a/packages/shared/src/i18n/en.json b/packages/shared/src/i18n/en.json index 72cca5d3..70de22ba 100644 --- a/packages/shared/src/i18n/en.json +++ b/packages/shared/src/i18n/en.json @@ -73,18 +73,13 @@ } }, "home": { - "hero": { - "subtitle": "Read classic literature. Build vocabulary. Track your progress.", - "cta": "Browse catalog" - }, "recentAuthors": { "title": "Recently Added Authors", "viewAll": "View all authors" }, "recentBooks": { "title": "Recently Added Books", - "viewAll": "View all books", - "read": "Read" + "viewAll": "View all books" } }, "firstRun": { @@ -110,28 +105,14 @@ "nav": { "catalog": "Catalog", "library": "Library", - "stats": "Stats", "vocabulary": "Vocabulary", "about": "About", - "browseBooks": "Browse all books", - "read": "Read", "discover": "Discover", "profile": "Profile" }, "onboarding": { - "chooseLanguage": "Choose your language", - "chooseLanguageSubtitle": "You can change this later in settings", "nativeLanguageTitle": "What language do you know best?", - "nativeLanguageSubtitle": "We'll translate into it while you read. You can change it any time in Profile.", - "learnByReading": "Learn by reading", - "tapToTranslate": "Tap any word to see its translation", - "saveWords": "Save words to build your vocabulary", - "save": "Save", - "ready": "You're ready!", - "readySubtitle": "Saved words appear in your vocabulary\nfor spaced repetition review", - "next": "Next", - "skip": "Skip", - "gotIt": "Got it" + "nativeLanguageSubtitle": "We'll translate into it while you read. You can change it any time in Profile." }, "profile": { "vocabReminder": { @@ -203,12 +184,6 @@ "library": { "title": "My Library", "signInPrompt": "Sign in to save books to your library and track your reading progress.", - "saved": "Saved", - "uploads": "Uploads", - "sortRecent": "Recently Added", - "sortTitle": "Title", - "sortProgress": "Progress", - "sortBy": "Sort by", "loading": "Loading...", "emptyLibrary": "Your library is empty.", "browseBooks": "Browse Books", @@ -229,14 +204,11 @@ "chapters": "chapters", "processing": "Processing...", "failed": "Failed", - "noUploads": "No uploaded books yet.", - "uploadHint": "Click the + button to upload EPUB or PDF files.", "timeJustNow": "just now", "timeMinAgo": "min ago", "timeHoursAgo": "hours ago", "timeYesterday": "yesterday", "timeDaysAgo": "days ago", - "failedToProcess": "Failed to process", "sidebar": { "all": "All books", "uploads": "My uploads", @@ -286,11 +258,7 @@ "progress": "Most read" }, "filter": { - "all": "All", - "reading": "Reading", "finished": "Finished", - "notStarted": "Not started", - "failed": "Failed", "empty": "No books match this filter", "clear": "Clear filter" }, @@ -300,24 +268,15 @@ "notStarted": "Not started", "failed": "Failed", "all": "All", - "empty": { - "reading": "Open a book to start reading", - "finished": "Finish your first book this month", - "notStarted": "Looking good — you're on top of your library", - "failed": "These uploads need attention", - "all": "No books match this filter" - }, "ariaLabel": "Filter by status" }, "search": { "placeholder": "Search your library…", "empty": "No books match \"{query}\"", - "clear": "Clear search", - "shortcut": "Cmd+F to focus" + "clear": "Clear search" }, "actions": { "menu": "Options", - "open": "Open", "viewDetails": "View details", "markFinished": "Mark as finished", "markUnfinished": "Mark as unfinished", @@ -326,7 +285,6 @@ "retrying": "Retrying…", "cancel": "Cancel", "cancelling": "Cancelling…", - "downloadOriginal": "Download original", "removeFromLibrary": "Remove from library", "addToCollection": "Add to collection", "addToCollectionEmpty": "No collections yet — create one below", @@ -388,13 +346,7 @@ }, "addMenu": { "title": "Add to library", - "ariaLabel": "Add content menu", - "comingSoon": "Coming in a future update", "uploadFile": "Upload file", - "pasteUrl": "Paste URL", - "emailBook": "Email a book", - "browserExtension": "Browser extension", - "mobileApps": "Mobile apps", "browseAll": "Browse all books" }, "books": { @@ -441,7 +393,6 @@ "downloading": "Downloading {progress}%...", "availableOffline": "Available offline", "downloadForOffline": "Download for offline", - "chaptersHeading": "Chapters", "readChapter": "Read {title}", "words": "words", "viewAllChapters": "View all {count} chapters", @@ -474,7 +425,6 @@ "title": "Search", "enterQuery": "Enter a search query", "noResults": "No results found", - "foundResults": "Found {total} results", "chapter": "Chapter", "readOnline": "Read {title} online", "readOnlineFree": "{title} - Read online free", @@ -555,13 +505,10 @@ "totalTime": "Total Time", "booksFinished": "Books Finished", "currentStreak": "Current Streak", - "avgDaily": "Avg Daily", "dailyGoal": "Daily Goal", "goalMet": "Goal met!", - "streakCalendar": "Reading Activity", "weeklyChart": "This Week", "achievements": "Achievements", - "goalSettings": "Goal Settings", "dailyMinutesTarget": "Daily minutes target", "streakThreshold": "Min minutes for streak", "saveGoal": "Save", @@ -588,11 +535,6 @@ "avgTimeToFinish": "Avg Days to Finish", "readingTimeByGenre": "Reading Time by Genre", "readingTimeByAuthor": "Reading Time by Author", - "slow": "Slow", - "medium": "Medium", - "fast": "Fast", - "short": "Short", - "long": "Long", "noData": "No data yet", "emptyTitle": "No reading stats yet", "emptySubtitle": "Start reading a book to track your progress", @@ -661,7 +603,6 @@ "clearConfirm": "Clear this chat? This can't be undone.", "loadingHistory": "Loading chat…", "detachQuote": "Remove quoted passage", - "askAboutThis": "Ask about this", "startersTitle": "Try asking", "starters": { "summary": "Summarize what I've read so far", @@ -670,9 +611,6 @@ "attention": "What should I pay attention to?" } }, - "reviewNow": "Review Now", - "later": "Later", - "wordsSaved": "{count} word(s) saved", "toastWordAdded": "Added to vocabulary", "toastWordAddedCount": "Added · {count} this session", "toastTapToReview": "Tap to review", @@ -687,7 +625,6 @@ }, "vocab": { "queuedForTomorrow": "Daily cap reached — queued for tomorrow", - "savedToReference": "Saved to reference (rare word)", "tapAgainToStudy": "Tap again to study ({n} left)", "addedToSrs": "Added to your vocabulary", "addAnywayFailed": "Couldn't add word — please try again", @@ -703,49 +640,18 @@ "under": "Less than a minute left", "minutes": "{minutes} min left", "hours": "{hours} hr left", - "hoursMinutes": "{hours} hr {minutes} min left", - "inChapter": "left in chapter", - "inBook": "left in book" + "hoursMinutes": "{hours} hr {minutes} min left" } }, - "review": { - "excellent": "Excellent!", - "greatWork": "Great work!", - "keepPracticing": "Keep practicing!", - "dayStreak": "{count} day streak" - }, "vocabulary": { "title": "Vocabulary", "signInPrompt": "Sign in to build your personal vocabulary list.", "empty": "No words saved yet. Select words while reading to add them.", - "saveWord": "Save word", - "wordSaved": "Word saved", - "alreadySaved": "Already saved", - "startReview": "Start Review", - "dueToday": "Due today", "totalWords": "Total words", "mastered": "Mastered", - "learning": "Learning", - "word": "Word", - "translation": "Translation", - "definition": "Definition", - "stage": "Stage", - "source": "Source", "dueDate": "Due", - "noReviewDue": "No words due for review!", "editTranslation": "Edit translation", - "stages": { - "0": "New", - "1": "Recognition", - "2": "Recall", - "3": "Context", - "4": "Mastered" - }, "filters": { - "all": "All", - "new": "New", - "learning": "Learning", - "mastered": "Mastered", "search": "Search words..." }, "sort": { @@ -755,31 +661,21 @@ "stage": "Stage" }, "review": { - "title": "Vocabulary Review", - "chooseTranslation": "Choose the correct translation", - "fillBlank": "What word fills the blank?", "correct": "Correct!", - "closeEnough": "Close enough!", "wrong": "Not quite", "correctAnswer": "Correct answer", "next": "Next", - "sessionComplete": "Session complete!", "reviewed": "Reviewed", "correctRate": "Correct rate", - "promoted": "Level up!", - "check": "Check", "fromBook": "from \"{title}\"", "backToVocab": "Back to Vocabulary" }, "stats": { - "reviewedToday": "Reviewed today", - "correctRate": "Correct rate", "streak": "Review streak" }, "weeklyBudget": { "label": "This week", "progress": "{used} / {budget}", - "remaining": "{n} left this week", "emptyStateSubtitle": "You've hit your weekly review target. Read — new reviews unlock as old ones age out.", "backToReading": "Back to reading" }, @@ -788,13 +684,7 @@ }, "practice": { "title": "Practice", - "cta": "Practice anyway", - "subtitle": "Doesn't affect your SRS schedule" - }, - "retired": { - "title": "Mastered — retired", - "subtitle": "Graduated from review. Tap to bring back.", - "unretire": "Bring back to review" + "cta": "Practice anyway" }, "clusters": { "bonusCta": "Bonus: {n} related words", diff --git a/packages/shared/src/i18n/unusedKeys.test.ts b/packages/shared/src/i18n/unusedKeys.test.ts new file mode 100644 index 00000000..1b5f3829 --- /dev/null +++ b/packages/shared/src/i18n/unusedKeys.test.ts @@ -0,0 +1,107 @@ +import { describe, it, expect } from 'vitest' +import { readFileSync, readdirSync, statSync } from 'node:fs' +import { resolve, join } from 'node:path' + +/** + * Keys this catalogue carries that neither app renders. + * + * Nothing checked this before and 100 had accumulated — a whole `review` namespace, + * most of `vocabulary`, `onboarding` copy for a flow that changed. They cost bundle + * size on a phone and, worse, they read as intent: the next person opening the file + * cannot tell which strings are actually shown. + * + * **It checks BOTH consumers, and that is not a detail.** The web app no longer keeps + * its own copy of a shared string, so a key used only by the website lives only here. + * A first pass at this test looked at mobile alone and reported 278 orphans; deleting + * them removed 244 keys the website renders, and `apps/web/src/locales/__tests__/ + * missing-keys.test.ts` failed instantly with `Footer.tsx → footer.privacy`. Since + * slice 3, "unused by mobile" and "unused" are different questions. + * + * **The detector is deliberately narrow, because a wrong answer deletes a working + * screen.** An earlier general version called `privacy.*` dead — 49 keys behind a + * Google Play store-listing URL — because they are reached through a shared list of + * key names rather than a literal `t('…')`. + * + * A key counts as referenced when EITHER the exact dotted path appears quoted + * anywhere in either app or here (covering `t('a.b')` and key tables such as + * `PRIVACY_SECTIONS`), OR it matches one of the templates below. + * + * DYNAMIC is the complete set of `` t(`…${…}`) `` shapes in both apps, taken by + * grepping for them rather than guessed. Add a new template to an app without adding + * it here and this test starts calling live keys dead — loudly, which is the point. + */ +const DYNAMIC = [ + // mobile + /^contact\.reachOut\w+$/, + /^library\.badge\.\w+$/, + /^library\.sort\.\w+$/, + /^library\.status\.\w+$/, + /^reader\.ask\.starters\.\w+$/, + /^terms\.use\w+$/, + /^tutor\.exercise\.\w+$/, + // web + /^home\.comparison\.(competitors|features)\.\w+$/, + /^home\.faq\.\w+\.[aq]$/, + /^home\.features\.\w+\.(title|description)$/, + /^home\.testimonials\.\w+\.(name|quote|role)$/, + /^library\.shelves\.\w+\.(title|subtitle)$/, + // web's DeviceVerifyPage builds `${ns}.${suffix}` with ns chosen at runtime + /^(deviceVerify|connectExtension)\./, +] + +/** + * Kept on purpose despite having no reference, `key: 'why'`. Empty today. Asserted by + * EQUALITY, so a key that starts being used must also be removed from here — an + * allowlist nobody prunes stops meaning anything. + */ +const KNOWN_UNREFERENCED: Record = {} + +const ROOTS = [ + resolve(__dirname, '..'), // packages/shared/src + resolve(__dirname, '../../../../apps/mobile/src'), + resolve(__dirname, '../../../../apps/mobile/app'), + resolve(__dirname, '../../../../apps/web/src'), + resolve(__dirname, '../../../../apps/web/scripts'), +] + +function read(dir: string, out: string[] = []): string[] { + for (const entry of readdirSync(dir)) { + if (['node_modules', 'dist', '.expo', '__fixtures__', 'locales'].includes(entry)) continue + const p = join(dir, entry) + let isDir: boolean + try { isDir = statSync(p).isDirectory() } catch { continue } // ios/Pods has broken symlinks + if (isDir) read(p, out) + else if (/\.(ts|tsx|js|mjs)$/.test(entry) && !/\.test\.tsx?$/.test(entry)) out.push(p) + } + return out +} + +const source = ROOTS.flatMap(d => read(d)).map(p => readFileSync(p, 'utf8')).join('\n') + +type Node = Record +const flatten = (n: Node, p = '', o: Record = {}) => { + for (const [k, v] of Object.entries(n)) { + const q = p ? `${p}.${k}` : k + if (v !== null && typeof v === 'object' && !Array.isArray(v)) flatten(v as Node, q, o) + else o[q] = v + } + return o +} + +const catalog = flatten(JSON.parse(readFileSync(resolve(__dirname, 'en.json'), 'utf8'))) + +describe('shared catalogue has no orphans', () => { + it('found both apps', () => { + // A walk that silently returns nothing would make every key look dead and this + // whole file pass for the worst possible reason. + expect(source.length).toBeGreaterThan(500_000) + }) + + it('every key is reachable from one of the apps, or written down as deliberately not', () => { + const unreferenced = Object.keys(catalog).filter(key => { + if (["'", '"', '`'].some(q => source.includes(`${q}${key}${q}`))) return false + return !DYNAMIC.some(rx => rx.test(key)) + }) + expect(unreferenced.sort()).toEqual(Object.keys(KNOWN_UNREFERENCED).sort()) + }) +})