@@ -59,26 +65,30 @@ export default function App(): JSX.Element { {/* Package Counter */}
-

- Showing {filteredPackages.length} package{filteredPackages.length !== 1 ? 's' : ''} -

+

{(count === 1) ? translate("App.package_count.single", { + count + }) : (count >= 2) && (count <= 4) ? translate("App.package_count.multiple", { + count + }) : translate("App.package_count.plural", { + count + })}

{/* Error State */} {error ? (
-

An error occurred while fetching packages: {error}

- +

{translate("App.error.fetching-packages", { + error + })}

+ +
) : filteredPackages.length === 0 ? ( // Empty State
-

No packages found matching your search.

+

{translate("App.no-packages-found")}

) : ( // Package List diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 58f89bf..ba7087d 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,18 +1,31 @@ import { - JSX + JSX, + useEffect } from 'react'; -import { ThemeToggle } from './ThemeToggle'; -import { Logo } from './Logo'; +import { + ThemeToggle +} from './ThemeToggle'; import { - Repository + Logo +} from './Logo'; + +import { + Repository, + Languages } from '../types'; import { MIRRORS } from '../services/api'; +import { + translate +} from '../i18n'; + +import i18next from 'i18next'; + interface HeaderProps { onRepositoryChange: (repo: Repository) => void; } @@ -23,12 +36,24 @@ export function Header({ const usedRepositories = new Set(Object.values(MIRRORS).map(mirror => mirror.repository)); const filteredRepository = Object.keys(Repository).reduce((acc, key) => { - // This code is flawed because it explicitly checks for 'ALL', reducing flexibility. - // A more generic approach should be used to filter unused repositories while preserving 'ALL'. if ((key === 'ALL') || usedRepositories.has(Repository[key as keyof typeof Repository])) acc[key] = Repository[key as keyof typeof Repository]; return acc; }, {} as Record); + const handleLanguageChange = (event: React.ChangeEvent) => { + const language = event.target.value; + + i18next.changeLanguage(language).then(() => { + localStorage.setItem('selectedLanguage', language); + window.location.reload(); + }); + }; + + useEffect(() => { + const savedLanguage = localStorage.getItem('selectedLanguage'); + if (savedLanguage && (savedLanguage !== i18next.language)) i18next.changeLanguage(savedLanguage); + }, []); + return (
@@ -46,7 +71,18 @@ export function Header({ (e) => onRepositoryChange(e.target.value as Repository) } defaultValue="all" className="bg-nord-5 dark:bg-nord-1 text-black dark:text-white border-2 border-nord-4 dark:border-nord-2 rounded-lg py-2 px-4 focus:ring-2 focus:ring-nord-8"> {Object.values(filteredRepository).map((repository) => ( - + + ))} + +
+ + {/* Language Dropdown */} +
+
diff --git a/src/components/InstallGuide.tsx b/src/components/InstallGuide.tsx index 2da65f2..865f375 100644 --- a/src/components/InstallGuide.tsx +++ b/src/components/InstallGuide.tsx @@ -5,6 +5,10 @@ import { import { Terminal, Copy, Check } from 'lucide-react'; +import { + translate +} from '../i18n'; + interface InstallGuideProps { packageName: string; } @@ -25,20 +29,13 @@ export function InstallGuide({ packageName }: InstallGuideProps): JSX.Element { {/* Title Section */}
- - Installation Command - + {translate("InstallGuide.installation-command")}
{/* Command Display Section */}
{command} -
); diff --git a/src/components/PackageCard.tsx b/src/components/PackageCard.tsx index b945718..03b8679 100644 --- a/src/components/PackageCard.tsx +++ b/src/components/PackageCard.tsx @@ -7,6 +7,10 @@ import { Box, ChevronDown, ChevronUp } from 'lucide-react'; import { Package } from '../types'; import { InstallGuide } from './InstallGuide'; +import { + translate +} from '../i18n'; + interface PackageCardProps { package: Package; } @@ -49,12 +53,12 @@ export function PackageCard({ package: pkg }: PackageCardProps): JSX.Element { {expanded ? ( <> - Hide Installation + {translate("PackageCard.hide-installation")} ) : ( <> - Show Installation + {translate("PackageCard.show-installation")} )} diff --git a/src/components/PackageCard/Badge.tsx b/src/components/PackageCard/Badge.tsx deleted file mode 100644 index 1dc932f..0000000 --- a/src/components/PackageCard/Badge.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { - JSX -} from 'react'; - -interface BadgeProps { - children: React.ReactNode; - color?: 'default' | 'success' | 'warning' | 'error'; // Add more colors as needed - size?: 'small' | 'medium' | 'large'; // Size options - ariaLabel?: string; // For accessibility -} - -const colorClasses = { - default: 'bg-gradient-to-r from-nord-7 to-nord-8/80 dark:from-nord-8/50 dark:to-nord-9/80 text-nord-0 dark:text-nord-6', - success: 'bg-green-500 text-white', - warning: 'bg-yellow-500 text-black', - error: 'bg-red-500 text-white', -}; - -const sizeClasses = { - small: 'text-xs px-2 py-1', - medium: 'text-sm px-3 py-1.5', - large: 'text-base px-4 py-2', -}; - -export function Badge({ children, color = 'default', size = 'medium', ariaLabel }: BadgeProps): JSX.Element { - const badgeColorClass = colorClasses[color] || colorClasses.default; - const badgeSizeClass = sizeClasses[size] || sizeClasses.medium; - - return ( - - {children} - - ); -} \ No newline at end of file diff --git a/src/components/PackageCard/ExpandButton.tsx b/src/components/PackageCard/ExpandButton.tsx deleted file mode 100644 index 79abfa0..0000000 --- a/src/components/PackageCard/ExpandButton.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { - JSX -} from 'react'; - -import { ChevronDown, ChevronUp } from 'lucide-react'; - -interface ExpandButtonProps { - expanded: boolean; - onClick: () => void; -} - -export function ExpandButton({ expanded, onClick }: ExpandButtonProps): JSX.Element { - return ( - - ); -} diff --git a/src/components/PackageCard/index.tsx b/src/components/PackageCard/index.tsx deleted file mode 100644 index b6ad601..0000000 --- a/src/components/PackageCard/index.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { - useState, - JSX -} from 'react'; - -import { Boxes } from 'lucide-react'; -import { Package } from '../../types'; -import { InstallGuide } from '../InstallGuide'; -import { Badge } from './Badge'; -import { ExpandButton } from './ExpandButton'; - -interface PackageCardProps { - package: Package; -} - -export function PackageCard({ package: pkg }: PackageCardProps): JSX.Element { - const [expanded, setExpanded] = useState(false); - - return ( -
- {/* Header Section */} -
-
- {/* Icon */} -
- -
- {/* Content */} -
-
-

- {pkg.name} -

- {pkg.version} -
-

- {pkg.description} -

-
- {pkg.repository} - setExpanded(!expanded)} - aria-label={expanded ? 'Collapse package details' : 'Expand package details'} - /> -
-
-
-
- - {/* Expandable Section */} - {expanded && ( -
- -
- )} -
- ); -} \ No newline at end of file diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index e8b6bb4..5cb6500 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -4,6 +4,10 @@ import { import { Search } from 'lucide-react'; +import { + translate +} from '../i18n'; + interface SearchBarProps { value: string; onChange: (value: string) => void; @@ -26,7 +30,7 @@ export function SearchBar({ type="text" value={value} onChange={(e) => onChange(e.target.value)} - placeholder="Search packages..." + placeholder={translate("SearchBar.placeholder")} className="block w-full pl-10 pr-4 py-3 border-2 border-nord-4 dark:border-nord-2 rounded-xl bg-nord-5 dark:bg-nord-1 focus:ring-2 focus:ring-nord-8 focus:border-transparent text-nord-0 dark:text-nord-6 placeholder-nord-3 dark:placeholder-nord-4 transition-all ease-in-out duration-200 shadow-md hover:shadow-lg" /> diff --git a/src/i18n.tsx b/src/i18n.tsx new file mode 100644 index 0000000..621cba2 --- /dev/null +++ b/src/i18n.tsx @@ -0,0 +1,46 @@ +import i18n from "i18next"; +import LanguageDetector from "i18next-browser-languagedetector"; + +import { + initReactI18next +} from "react-i18next"; + +import { + ENGLISH +} from './locales/en'; + +import { + UKRAINIAN +} from './locales/uk'; + +const resources = { + en: { + translation: ENGLISH + }, + + uk: { + translation: UKRAINIAN + } +}; + +i18n.use(initReactI18next).use(LanguageDetector).init({ + resources, + fallbackLng: (localStorage.getItem('selectedLanguage') || "en"), + + interpolation: { + escapeValue: false + }, + + detection: { + order: ['localStorage', 'navigator'], + caches: ['localStorage'] + } +}); + +export const translate = (key: string, options?: { + [key: string]: any +}): string => { + return i18n.t(key, options); +}; + +export default i18n; diff --git a/src/locales/en.tsx b/src/locales/en.tsx new file mode 100644 index 0000000..839125d --- /dev/null +++ b/src/locales/en.tsx @@ -0,0 +1,49 @@ +export const ENGLISH = { + // src/components/Header.tsx + Header: { + "all-repositories": "All Repositories" + }, + + // src/components/InstallGuide.tsx + InstallGuide: { + "installation-command": "Installation Command", + + "copy": { + "prompt": "Copy to clipboard", + "success": "Copied!" + } + }, + + // src/components/Logo.tsx + Logo: { + "title": "Snigdha OS Package List", + "description": "Browse and search through the official Snigdha OS package repository", + }, + + // src/components/PackageCard.tsx + PackageCard: { + "show-installation": "Show Installation", + "hide-installation": "Hide Installation" + }, + + // src/components/SearchBar.tsx + SearchBar: { + "placeholder": "Search packages..." + }, + + // src/App.tsx + App: { + "package_count": { + "single": "Showing {{count}} package", + "multiple": "Showing {{count}} packages", + "plural": "Showing {{count}} packages", + }, + + error: { + "fetching-packages": "An error occurred while fetching packages: {{error}}", + "retry-fetching-packages": "Retry" + }, + + "no-packages-found": "No packages found matching your search." + } +}; diff --git a/src/locales/uk.tsx b/src/locales/uk.tsx new file mode 100644 index 0000000..9e11afd --- /dev/null +++ b/src/locales/uk.tsx @@ -0,0 +1,49 @@ +export const UKRAINIAN = { + // src/components/Header.tsx + Header: { + "all-repositories": "Усі репозиторії" + }, + + // src/components/InstallGuide.tsx + InstallGuide: { + "installation-command": "Команда для встановлення", + + "copy": { + "prompt": "Копіювати в буфер обміну", + "success": "Скопійовано!" + } + }, + + // src/components/Logo.tsx + Logo: { + "title": "Список пакетів Snigdha OS", + "description": "Переглядайте та шукайте в офіційному репозиторії пакетів Snigdha OS", + }, + + // src/components/PackageCard.tsx + PackageCard: { + "show-installation": "Показати інструкцію з встановлення", + "hide-installation": "Сховати інструкцію з встановлення" + }, + + // src/components/SearchBar.tsx + SearchBar: { + "placeholder": "Шукати пакети..." + }, + + // src/App.tsx + App: { + "package_count": { + "single": "Показано {{count}} пакет", + "multiple": "Показано {{count}} пакети", + "plural": "Показано {{count}} пакетів" + }, + + error: { + "fetching-packages": "Сталася помилка при отриманні пакетів: {{error}}", + "retry-fetching-packages": "Спробувати ще раз" + }, + + "no-packages-found": "Не знайдено жодного пакета, що відповідає вашому запиту." + } +}; diff --git a/src/main.tsx b/src/main.tsx index ea9e363..b60a492 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,17 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; +import { + StrictMode +} from 'react'; + +import { + createRoot +} from 'react-dom/client'; + import App from './App.tsx'; import './index.css'; +import './i18n'; createRoot(document.getElementById('root')!).render( - - - + + + ); diff --git a/src/types.ts b/src/types.ts index 710c437..efff439 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,6 +10,12 @@ export enum Repository { // Type alias for UI themes export type Theme = ('light' | 'dark'); +// Enumeration of supported languages +export enum Languages { + EN = "English", + UK = "Українська" +} + // Interface representing a single package export interface Package { /** The name of the package */