From 73c88cc6aae74ca2933b9dccc1ab0854a8a09286 Mon Sep 17 00:00:00 2001 From: mukimovd <41473651-mukimovd@users.noreply.replit.com> Date: Thu, 1 May 2025 03:26:26 +0000 Subject: [PATCH] Improve language selection and reading experience for translated works Enhance the language selection UI and reading view to support multiple translations using i18n features. Replit-Commit-Author: Agent Replit-Commit-Session-Id: cbacfb18-842a-4116-a907-18c0105ad8ec Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/39b5c689-6e8a-4d5a-9792-69cc81a56534/32087fef-c332-4acc-a7da-d0e55b5b0716.jpg --- .../reading/EnhancedReadingView.tsx | 25 +- .../reading/TranslationSelector.tsx | 343 +++++++++++++----- client/src/hooks/use-media-query.ts | 10 +- 3 files changed, 287 insertions(+), 91 deletions(-) diff --git a/client/src/components/reading/EnhancedReadingView.tsx b/client/src/components/reading/EnhancedReadingView.tsx index 3b23b89..bdac288 100644 --- a/client/src/components/reading/EnhancedReadingView.tsx +++ b/client/src/components/reading/EnhancedReadingView.tsx @@ -58,10 +58,18 @@ export function EnhancedReadingView({ work, translations }: EnhancedReadingViewP // Get the selected translation const selectedTranslation = translations.find(t => t.id === selectedTranslationId); + + // Determine if original text is selected + const isOriginalSelected = !selectedTranslationId; // Content to display - either the translation or original work const contentToDisplay = selectedTranslation ? selectedTranslation.content : work.content; + // Handler for viewing original text + const handleViewOriginal = () => { + setSelectedTranslationId(undefined); + }; + // Check if there's a line number in the URL hash useEffect(() => { if (window.location.hash) { @@ -504,14 +512,15 @@ export function EnhancedReadingView({ work, translations }: EnhancedReadingViewP - {translations.length > 0 && ( - - )} + {/* Text content with enhanced annotation features */} diff --git a/client/src/components/reading/TranslationSelector.tsx b/client/src/components/reading/TranslationSelector.tsx index d35a868..7b42b19 100644 --- a/client/src/components/reading/TranslationSelector.tsx +++ b/client/src/components/reading/TranslationSelector.tsx @@ -1,118 +1,299 @@ +import { TranslationWithDetails } from "@/lib/types"; import { Translation } from "@shared/schema"; import { Button } from "@/components/ui/button"; import { Link } from "wouter"; -import { Languages } from "lucide-react"; +import { Languages, ChevronDown, Globe, BookOpen, BookMarked } from "lucide-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + SelectGroup, + SelectLabel +} from "@/components/ui/select"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs"; +import { useState } from "react"; +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; interface TranslationSelectorProps { - translations: Translation[]; + translations: TranslationWithDetails[]; currentTranslationId?: number; workSlug: string; + workLanguage: string; onSelectTranslation: (translationId: number) => void; + onViewOriginal: () => void; + isOriginalSelected: boolean; } +// Language display names for common languages +const languageNames: Record = { + 'en': 'English', + 'fr': 'French (Français)', + 'es': 'Spanish (Español)', + 'de': 'German (Deutsch)', + 'it': 'Italian (Italiano)', + 'pt': 'Portuguese (Português)', + 'ru': 'Russian (Русский)', + 'zh': 'Chinese (中文)', + 'ja': 'Japanese (日本語)', + 'ko': 'Korean (한국어)', + 'ar': 'Arabic (العربية)', + 'hi': 'Hindi (हिन्दी)', + 'bn': 'Bengali (বাংলা)', +}; + +// Helper function to get display name +const getLanguageDisplayName = (code: string): string => { + return languageNames[code.toLowerCase()] || code; +}; + export function TranslationSelector({ translations, currentTranslationId, workSlug, - onSelectTranslation + workLanguage, + onSelectTranslation, + onViewOriginal, + isOriginalSelected }: TranslationSelectorProps) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + // Group translations by language - const translationsByLanguage = translations.reduce>((acc, translation) => { + const translationsByLanguage = translations.reduce>((acc, translation) => { if (!acc[translation.language]) { acc[translation.language] = []; } acc[translation.language].push(translation); return acc; }, {}); + + // Get current translation + const currentTranslation = translations.find(t => t.id === currentTranslationId); + + // Count languages for display + const languageCount = Object.keys(translationsByLanguage).length; + + // Format date/year for display + const formatYear = (year?: number) => { + if (!year) return ''; + return year.toString(); + }; return ( -
-
- {Object.entries(translationsByLanguage).map(([language, languageTranslations]) => { - // Find if any translation in this language is currently selected - const isLanguageSelected = languageTranslations.some(t => t.id === currentTranslationId); - - return ( -
+
+

+ + Language Options + + {languageCount + 1} languages + +

+ +
+ {/* Original language button */} + + + {/* Simple mobile-friendly dropdown for all translations */} + + + {/* Advanced translation selector with Popover */} + + + + + + + + By Language + By Translator + + + + +
+ {Object.entries(translationsByLanguage).map(([language, languageTranslations]) => ( +
+

{getLanguageDisplayName(language)}

+
+ {languageTranslations.map(translation => ( + + ))} +
+
+ ))} +
+
+
+ + + +
+ {translations.map(translation => ( + + ))} +
+
+
+
+ +
+ + + +
+
+
+ + {/* Quick language pills for most common languages */} +
+ {Object.keys(translationsByLanguage).slice(0, 5).map(language => { + const languageTranslations = translationsByLanguage[language]; + const isLanguageSelected = languageTranslations.some(t => t.id === currentTranslationId); + + return ( - - {/* Dropdown for multiple translations of the same language */} - {languageTranslations.length > 1 && ( -
- {languageTranslations.map(translation => ( - - ))} -
- )} -
- ); - })} - - - - + +{Object.keys(translationsByLanguage).length - 5} more + + )} +
); diff --git a/client/src/hooks/use-media-query.ts b/client/src/hooks/use-media-query.ts index 22c572b..2acf6ec 100644 --- a/client/src/hooks/use-media-query.ts +++ b/client/src/hooks/use-media-query.ts @@ -1,15 +1,21 @@ import { useState, useEffect } from 'react'; +/** + * Custom hook that returns whether a media query matches + * @param query The media query to check + * @returns True if the media query matches, false otherwise + */ export function useMediaQuery(query: string): boolean { const [matches, setMatches] = useState(false); useEffect(() => { + // Create the media query list const mediaQuery = window.matchMedia(query); // Set initial value setMatches(mediaQuery.matches); - // Define callback function to handle changes + // Define a callback function to handle changes const handleChange = (event: MediaQueryListEvent) => { setMatches(event.matches); }; @@ -21,7 +27,7 @@ export function useMediaQuery(query: string): boolean { return () => { mediaQuery.removeEventListener('change', handleChange); }; - }, [query]); + }, [query]); // Only re-run if the query changes return matches; } \ No newline at end of file