mirror of
https://github.com/Snigdha-OS/package-browser.git
synced 2025-09-05 12:16:42 +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",
|
||||
"lucide-react": "^0.471.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": {
|
||||
"@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
|
||||
} from './types';
|
||||
|
||||
import {
|
||||
translate
|
||||
} from './i18n';
|
||||
|
||||
export default function App(): JSX.Element {
|
||||
const { packages, loading, error } = usePackages();
|
||||
const [search, setSearch] = useState('');
|
||||
@@ -47,6 +51,8 @@ export default function App(): JSX.Element {
|
||||
setSelectedRepository(repo);
|
||||
};
|
||||
|
||||
const count = filteredPackages.length;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-nord-6 dark:bg-nord-0 transition-colors" role="main">
|
||||
<Header onRepositoryChange={handleRepositoryFilterChange} />
|
||||
@@ -59,26 +65,30 @@ export default function App(): JSX.Element {
|
||||
|
||||
{/* Package Counter */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-sm text-nord-3 dark:text-nord-4" aria-live="polite">
|
||||
Showing {filteredPackages.length} package{filteredPackages.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
<p className="text-sm text-nord-3 dark:text-nord-4" aria-live="polite">{(count === 1) ? translate("App.package_count.single", {
|
||||
count
|
||||
}) : (count >= 2) && (count <= 4) ? translate("App.package_count.multiple", {
|
||||
count
|
||||
}) : translate("App.package_count.plural", {
|
||||
count
|
||||
})}</p>
|
||||
</div>
|
||||
|
||||
{/* Error State */}
|
||||
{error ? (
|
||||
<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>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="mt-2 inline-block text-sm text-nord-10 hover:underline"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
<p>{translate("App.error.fetching-packages", {
|
||||
error
|
||||
})}</p>
|
||||
|
||||
<button onClick={
|
||||
() => window.location.reload()
|
||||
} className="mt-2 inline-block text-sm text-nord-10 hover:underline">{translate("App.error.retry-fetching-packages")}</button>
|
||||
</div>
|
||||
) : filteredPackages.length === 0 ? (
|
||||
// Empty State
|
||||
<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>
|
||||
) : (
|
||||
// Package List
|
||||
|
@@ -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<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 (
|
||||
<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">
|
||||
@@ -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) => (
|
||||
<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>
|
||||
</div>
|
||||
|
@@ -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 */}
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<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">
|
||||
Installation Command
|
||||
</span>
|
||||
<span className="text-nord-2 dark:text-nord-5 text-sm font-semibold uppercase tracking-wide">{translate("InstallGuide.installation-command")}</span>
|
||||
</div>
|
||||
|
||||
{/* Command Display Section */}
|
||||
<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>
|
||||
<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 ? 'Copied!' : 'Copy to clipboard'}
|
||||
aria-label={copied ? 'Copied' : 'Copy command'}
|
||||
>
|
||||
<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")}>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4 text-nord-6 dark:text-nord-0" />
|
||||
) : (
|
||||
|
@@ -2,6 +2,10 @@ import {
|
||||
JSX
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
translate
|
||||
} from '../i18n';
|
||||
|
||||
export function Logo(): JSX.Element {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -22,10 +26,8 @@ export function Logo(): JSX.Element {
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">Snigdha OS Package List</h1>
|
||||
<p className="text-nord-4 text-sm mt-1">
|
||||
Browse and search through the official Snigdha OS package repository
|
||||
</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight">{translate("Logo.title")}</h1>
|
||||
<p className="text-nord-4 text-sm mt-1">{translate("Logo.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -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 ? (
|
||||
<>
|
||||
<ChevronUp className="h-5 w-5 animate-bounce" />
|
||||
Hide Installation
|
||||
{translate("PackageCard.hide-installation")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="h-5 w-5 animate-bounce" />
|
||||
Show Installation
|
||||
{translate("PackageCard.show-installation")}
|
||||
</>
|
||||
)}
|
||||
</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 {
|
||||
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"
|
||||
/>
|
||||
|
||||
|
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": "Не знайдено жодного пакета, що відповідає вашому запиту."
|
||||
}
|
||||
};
|
17
src/main.tsx
17
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(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
|
@@ -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 */
|
||||
|
Reference in New Issue
Block a user