mirror of
https://github.com/Snigdha-OS/package-browser.git
synced 2025-09-06 04:35:17 +02:00
feat: Add language support (i18n)
This commit is contained in:
4051
package-lock.json
generated
4051
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,11 @@
|
|||||||
"gh-pages": "^6.3.0",
|
"gh-pages": "^6.3.0",
|
||||||
"lucide-react": "^0.471.0",
|
"lucide-react": "^0.471.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"i18next": "^24.2.1",
|
||||||
|
"react-i18next": "^15.4.0",
|
||||||
|
"i18next-http-backend": "^3.0.1",
|
||||||
|
"i18next-browser-languagedetector": "^8.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
|
2776
pnpm-lock.yaml
generated
2776
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
32
src/App.tsx
32
src/App.tsx
@@ -13,6 +13,10 @@ import {
|
|||||||
Repository
|
Repository
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
translate
|
||||||
|
} from './i18n';
|
||||||
|
|
||||||
export default function App(): JSX.Element {
|
export default function App(): JSX.Element {
|
||||||
const { packages, loading, error } = usePackages();
|
const { packages, loading, error } = usePackages();
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
@@ -47,6 +51,8 @@ export default function App(): JSX.Element {
|
|||||||
setSelectedRepository(repo);
|
setSelectedRepository(repo);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const count = filteredPackages.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-nord-6 dark:bg-nord-0 transition-colors" role="main">
|
<div className="min-h-screen bg-nord-6 dark:bg-nord-0 transition-colors" role="main">
|
||||||
<Header onRepositoryChange={handleRepositoryFilterChange} />
|
<Header onRepositoryChange={handleRepositoryFilterChange} />
|
||||||
@@ -59,26 +65,30 @@ export default function App(): JSX.Element {
|
|||||||
|
|
||||||
{/* Package Counter */}
|
{/* Package Counter */}
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<p className="text-sm text-nord-3 dark:text-nord-4" aria-live="polite">
|
<p className="text-sm text-nord-3 dark:text-nord-4" aria-live="polite">{(count === 1) ? translate("App.package_count.single", {
|
||||||
Showing {filteredPackages.length} package{filteredPackages.length !== 1 ? 's' : ''}
|
count
|
||||||
</p>
|
}) : (count >= 2) && (count <= 4) ? translate("App.package_count.multiple", {
|
||||||
|
count
|
||||||
|
}) : translate("App.package_count.plural", {
|
||||||
|
count
|
||||||
|
})}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error State */}
|
{/* Error State */}
|
||||||
{error ? (
|
{error ? (
|
||||||
<div className="rounded-lg bg-nord-11/10 dark:bg-nord-11/20 p-4 text-nord-11" role="alert">
|
<div className="rounded-lg bg-nord-11/10 dark:bg-nord-11/20 p-4 text-nord-11" role="alert">
|
||||||
<p>An error occurred while fetching packages: {error}</p>
|
<p>{translate("App.error.fetching-packages", {
|
||||||
<button
|
error
|
||||||
onClick={() => window.location.reload()}
|
})}</p>
|
||||||
className="mt-2 inline-block text-sm text-nord-10 hover:underline"
|
|
||||||
>
|
<button onClick={
|
||||||
Retry
|
() => window.location.reload()
|
||||||
</button>
|
} className="mt-2 inline-block text-sm text-nord-10 hover:underline">{translate("App.error.retry-fetching-packages")}</button>
|
||||||
</div>
|
</div>
|
||||||
) : filteredPackages.length === 0 ? (
|
) : filteredPackages.length === 0 ? (
|
||||||
// Empty State
|
// Empty State
|
||||||
<div className="text-center text-nord-3 dark:text-nord-4 mt-12">
|
<div className="text-center text-nord-3 dark:text-nord-4 mt-12">
|
||||||
<p>No packages found matching your search.</p>
|
<p>{translate("App.no-packages-found")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// Package List
|
// Package List
|
||||||
|
@@ -1,18 +1,31 @@
|
|||||||
import {
|
import {
|
||||||
JSX
|
JSX,
|
||||||
|
useEffect
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { ThemeToggle } from './ThemeToggle';
|
import {
|
||||||
import { Logo } from './Logo';
|
ThemeToggle
|
||||||
|
} from './ThemeToggle';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Repository
|
Logo
|
||||||
|
} from './Logo';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Repository,
|
||||||
|
Languages
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MIRRORS
|
MIRRORS
|
||||||
} from '../services/api';
|
} from '../services/api';
|
||||||
|
|
||||||
|
import {
|
||||||
|
translate
|
||||||
|
} from '../i18n';
|
||||||
|
|
||||||
|
import i18next from 'i18next';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
onRepositoryChange: (repo: Repository) => void;
|
onRepositoryChange: (repo: Repository) => void;
|
||||||
}
|
}
|
||||||
@@ -23,12 +36,24 @@ export function Header({
|
|||||||
const usedRepositories = new Set(Object.values(MIRRORS).map(mirror => mirror.repository));
|
const usedRepositories = new Set(Object.values(MIRRORS).map(mirror => mirror.repository));
|
||||||
|
|
||||||
const filteredRepository = Object.keys(Repository).reduce((acc, key) => {
|
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];
|
if ((key === 'ALL') || usedRepositories.has(Repository[key as keyof typeof Repository])) acc[key] = Repository[key as keyof typeof Repository];
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, string>);
|
}, {} as Record<string, string>);
|
||||||
|
|
||||||
|
const handleLanguageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
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 (
|
return (
|
||||||
<header className="bg-gradient-to-r from-nord-9 to-nord-8 via-nord-10 text-nord-6 shadow-lg">
|
<header className="bg-gradient-to-r from-nord-9 to-nord-8 via-nord-10 text-nord-6 shadow-lg">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
@@ -46,7 +71,18 @@ export function Header({
|
|||||||
(e) => onRepositoryChange(e.target.value as Repository)
|
(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">
|
} 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) => (
|
{Object.values(filteredRepository).map((repository) => (
|
||||||
<option key={repository} value={repository}>{((repository === Repository.ALL) ? 'All Repositories' : repository.charAt(0).toUpperCase() + repository.slice(1))}</option>
|
<option key={repository} value={repository}>{((repository === Repository.ALL) ? translate("Header.all-repositories") : repository.charAt(0).toUpperCase() + repository.slice(1))}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Language Dropdown */}
|
||||||
|
<div>
|
||||||
|
<select onChange={handleLanguageChange} defaultValue={i18next.language} 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.entries(Languages).map(([key, label]) => (
|
||||||
|
<option key={key} value={key.toLowerCase()}>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -5,6 +5,10 @@ import {
|
|||||||
|
|
||||||
import { Terminal, Copy, Check } from 'lucide-react';
|
import { Terminal, Copy, Check } from 'lucide-react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
translate
|
||||||
|
} from '../i18n';
|
||||||
|
|
||||||
interface InstallGuideProps {
|
interface InstallGuideProps {
|
||||||
packageName: string;
|
packageName: string;
|
||||||
}
|
}
|
||||||
@@ -25,20 +29,13 @@ export function InstallGuide({ packageName }: InstallGuideProps): JSX.Element {
|
|||||||
{/* Title Section */}
|
{/* Title Section */}
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<Terminal className="h-5 w-5 text-nord-9 dark:text-nord-7" />
|
<Terminal className="h-5 w-5 text-nord-9 dark:text-nord-7" />
|
||||||
<span className="text-nord-2 dark:text-nord-5 text-sm font-semibold uppercase tracking-wide">
|
<span className="text-nord-2 dark:text-nord-5 text-sm font-semibold uppercase tracking-wide">{translate("InstallGuide.installation-command")}</span>
|
||||||
Installation Command
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Command Display Section */}
|
{/* Command Display Section */}
|
||||||
<div className="flex items-center justify-between bg-nord-4 dark:bg-nord-2 rounded-lg px-5 py-3">
|
<div className="flex items-center justify-between bg-nord-4 dark:bg-nord-2 rounded-lg px-5 py-3">
|
||||||
<code className="text-nord-8 dark:text-nord-6 font-mono text-sm truncate">{command}</code>
|
<code className="text-nord-8 dark:text-nord-6 font-mono text-sm truncate">{command}</code>
|
||||||
<button
|
<button onClick={copyCommand} className="flex items-center justify-center w-8 h-8 rounded-full bg-nord-3 hover:bg-nord-7 dark:bg-nord-5 dark:hover:bg-nord-8 transition-colors" title={copied ? translate("InstallGuide.copy.success") : translate("InstallGuide.copy.prompt")}>
|
||||||
onClick={copyCommand}
|
|
||||||
className="flex items-center justify-center w-8 h-8 rounded-full bg-nord-3 hover:bg-nord-7 dark:bg-nord-5 dark:hover:bg-nord-8 transition-colors"
|
|
||||||
title={copied ? 'Copied!' : 'Copy to clipboard'}
|
|
||||||
aria-label={copied ? 'Copied' : 'Copy command'}
|
|
||||||
>
|
|
||||||
{copied ? (
|
{copied ? (
|
||||||
<Check className="h-4 w-4 text-nord-6 dark:text-nord-0" />
|
<Check className="h-4 w-4 text-nord-6 dark:text-nord-0" />
|
||||||
) : (
|
) : (
|
||||||
|
@@ -2,6 +2,10 @@ import {
|
|||||||
JSX
|
JSX
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
translate
|
||||||
|
} from '../i18n';
|
||||||
|
|
||||||
export function Logo(): JSX.Element {
|
export function Logo(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -22,10 +26,8 @@ export function Logo(): JSX.Element {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Snigdha OS Package List</h1>
|
<h1 className="text-2xl font-bold tracking-tight">{translate("Logo.title")}</h1>
|
||||||
<p className="text-nord-4 text-sm mt-1">
|
<p className="text-nord-4 text-sm mt-1">{translate("Logo.description")}</p>
|
||||||
Browse and search through the official Snigdha OS package repository
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -7,6 +7,10 @@ import { Box, ChevronDown, ChevronUp } from 'lucide-react';
|
|||||||
import { Package } from '../types';
|
import { Package } from '../types';
|
||||||
import { InstallGuide } from './InstallGuide';
|
import { InstallGuide } from './InstallGuide';
|
||||||
|
|
||||||
|
import {
|
||||||
|
translate
|
||||||
|
} from '../i18n';
|
||||||
|
|
||||||
interface PackageCardProps {
|
interface PackageCardProps {
|
||||||
package: Package;
|
package: Package;
|
||||||
}
|
}
|
||||||
@@ -49,12 +53,12 @@ export function PackageCard({ package: pkg }: PackageCardProps): JSX.Element {
|
|||||||
{expanded ? (
|
{expanded ? (
|
||||||
<>
|
<>
|
||||||
<ChevronUp className="h-5 w-5 animate-bounce" />
|
<ChevronUp className="h-5 w-5 animate-bounce" />
|
||||||
Hide Installation
|
{translate("PackageCard.hide-installation")}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ChevronDown className="h-5 w-5 animate-bounce" />
|
<ChevronDown className="h-5 w-5 animate-bounce" />
|
||||||
Show Installation
|
{translate("PackageCard.show-installation")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
@@ -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 (
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center rounded-full font-semibold shadow-md hover:shadow-lg transition-all duration-300 ${badgeColorClass} ${badgeSizeClass}`}
|
|
||||||
aria-label={ariaLabel}
|
|
||||||
title={ariaLabel} // Optional: Add title for tooltips
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -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 (
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
className="flex items-center gap-2 px-3 py-1 text-sm font-medium text-nord-9 dark:text-nord-8 hover:text-nord-10 dark:hover:text-nord-7 focus:outline-none focus:ring-2 focus:ring-nord-8 dark:focus:ring-nord-9 rounded-lg transition-all duration-300"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`transform transition-transform duration-300 ${
|
|
||||||
expanded ? 'rotate-180' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{expanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
|
||||||
</span>
|
|
||||||
{expanded ? 'Hide installation' : 'Show installation'}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -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 (
|
|
||||||
<div className="group bg-nord-5 dark:bg-nord-1 rounded-xl shadow-lg border border-nord-4 dark:border-nord-2 hover:shadow-xl transition-all duration-300 overflow-hidden">
|
|
||||||
{/* Header Section */}
|
|
||||||
<div className="p-5">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
{/* Icon */}
|
|
||||||
<div className="p-3 bg-nord-7/10 dark:bg-nord-7/20 rounded-full shadow-md group-hover:scale-105 transition-transform">
|
|
||||||
<Boxes className="h-6 w-6 text-nord-7" />
|
|
||||||
</div>
|
|
||||||
{/* Content */}
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h3 className="font-semibold text-lg text-nord-0 dark:text-nord-6 group-hover:text-nord-8 transition-colors">
|
|
||||||
{pkg.name}
|
|
||||||
</h3>
|
|
||||||
<span className="text-sm text-nord-3 dark:text-nord-4">{pkg.version}</span>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 text-sm text-nord-2 dark:text-nord-4 line-clamp-2">
|
|
||||||
{pkg.description}
|
|
||||||
</p>
|
|
||||||
<div className="mt-3 flex items-center justify-between">
|
|
||||||
<Badge>{pkg.repository}</Badge>
|
|
||||||
<ExpandButton
|
|
||||||
expanded={expanded}
|
|
||||||
onClick={() => setExpanded(!expanded)}
|
|
||||||
aria-label={expanded ? 'Collapse package details' : 'Expand package details'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Expandable Section */}
|
|
||||||
{expanded && (
|
|
||||||
<div className="border-t border-nord-4 dark:border-nord-2 bg-nord-6 dark:bg-nord-0 p-5 transition-opacity duration-300">
|
|
||||||
<InstallGuide packageName={pkg.name} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -4,6 +4,10 @@ import {
|
|||||||
|
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
translate
|
||||||
|
} from '../i18n';
|
||||||
|
|
||||||
interface SearchBarProps {
|
interface SearchBarProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
@@ -26,7 +30,7 @@ export function SearchBar({
|
|||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.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"
|
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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
46
src/i18n.tsx
Normal file
46
src/i18n.tsx
Normal file
@@ -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;
|
49
src/locales/en.tsx
Normal file
49
src/locales/en.tsx
Normal file
@@ -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."
|
||||||
|
}
|
||||||
|
};
|
49
src/locales/uk.tsx
Normal file
49
src/locales/uk.tsx
Normal file
@@ -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": "Не знайдено жодного пакета, що відповідає вашому запиту."
|
||||||
|
}
|
||||||
|
};
|
11
src/main.tsx
11
src/main.tsx
@@ -1,7 +1,14 @@
|
|||||||
import { StrictMode } from 'react';
|
import {
|
||||||
import { createRoot } from 'react-dom/client';
|
StrictMode
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createRoot
|
||||||
|
} from 'react-dom/client';
|
||||||
|
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
import './i18n';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
@@ -10,6 +10,12 @@ export enum Repository {
|
|||||||
// Type alias for UI themes
|
// Type alias for UI themes
|
||||||
export type Theme = ('light' | 'dark');
|
export type Theme = ('light' | 'dark');
|
||||||
|
|
||||||
|
// Enumeration of supported languages
|
||||||
|
export enum Languages {
|
||||||
|
EN = "English",
|
||||||
|
UK = "Українська"
|
||||||
|
}
|
||||||
|
|
||||||
// Interface representing a single package
|
// Interface representing a single package
|
||||||
export interface Package {
|
export interface Package {
|
||||||
/** The name of the package */
|
/** The name of the package */
|
||||||
|
Reference in New Issue
Block a user