From e82366e5e809ba5e8f40f9d5a78fb7df387affc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C4=B0pek?= Date: Mon, 10 Nov 2025 17:44:04 +0300 Subject: [PATCH] feat: add internationalization support to package browser UI - Integrated locale context throughout PackageBrowser, PlatformSelector, SelectionManager, and RepoHubApp components - Replaced all hardcoded UI strings with translation keys (t() function calls) - Updated RepoHubApp to wrap content in LocaleProvider for i18n support --- src/components/Header.tsx | 76 +++++++++++ src/components/PackageBrowser.tsx | 26 ++-- src/components/PlatformSelector.tsx | 29 +++-- src/components/RepoHubApp.tsx | 28 +++-- src/components/SelectionManager.tsx | 16 +-- src/contexts/LocaleContext.tsx | 189 ++++++++++++++++++++++++++++ src/hooks/useLocale.ts | 173 +++++++++++++++++++++++++ src/hooks/useTheme.ts | 65 ++++++++++ 8 files changed, 563 insertions(+), 39 deletions(-) create mode 100644 src/components/Header.tsx create mode 100644 src/contexts/LocaleContext.tsx create mode 100644 src/hooks/useLocale.ts create mode 100644 src/hooks/useTheme.ts diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..dd925c5 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,76 @@ +"use client" + +import { Button } from '@/components/ui/button' +import { useTheme } from '@/hooks/useTheme' +import { useLocale } from '@/contexts/LocaleContext' +import { Sun, Moon, Monitor, Globe } from 'lucide-react' + +export function Header() { + const { theme, isDark, toggleTheme } = useTheme() + const { locale, toggleLocale } = useLocale() + + const getThemeIcon = () => { + switch (theme) { + case 'light': + return + case 'dark': + return + default: + return + } + } + + const getThemeLabel = () => { + switch (theme) { + case 'light': + return locale === 'tr' ? 'Aydınlık' : 'Light' + case 'dark': + return locale === 'tr' ? 'Karanlık' : 'Dark' + default: + return locale === 'tr' ? 'Sistem' : 'System' + } + } + + return ( +
+
+
+
+ RH +
+ + RepoHub + +
+ +
+ {/* Theme Toggle */} + + + {/* Language Toggle */} + +
+
+
+ ) +} diff --git a/src/components/PackageBrowser.tsx b/src/components/PackageBrowser.tsx index d880514..ede2fcf 100644 --- a/src/components/PackageBrowser.tsx +++ b/src/components/PackageBrowser.tsx @@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { mockPackages, categories, licenses } from '@/data/mockData' +import { useLocale } from '@/contexts/LocaleContext' import { Package, FilterOptions, Platform } from '@/types' import { Search, Package as PackageIcon, Terminal, Monitor } from 'lucide-react' @@ -22,6 +23,7 @@ export function PackageBrowser({ onPackageToggle, onFiltersChange }: PackageBrowserProps) { + const { t } = useLocale() const [searchQuery, setSearchQuery] = useState('') const [filters, setFilters] = useState({ platforms: [], @@ -83,7 +85,7 @@ export function PackageBrowser({ return ( -

Please select a platform first

+

{t('platform.description')}

) @@ -103,7 +105,7 @@ export function PackageBrowser({ setSearchQuery(e.target.value)} @@ -115,7 +117,7 @@ export function PackageBrowser({ handleFilterChange('categories', value ? [value] : []) }> - + {categories.map(category => ( @@ -131,11 +133,11 @@ export function PackageBrowser({ handleFilterChange('types', value ? [value as 'gui' | 'cli'] : []) }> - + - GUI Applications - CLI Tools + {t('packages.filters.gui')} + {t('packages.filters.cli')} @@ -144,11 +146,11 @@ export function PackageBrowser({ handleFilterChange('repositories', value ? [value as 'official' | 'third-party'] : []) }> - + - Official Only - Third Party + {t('packages.filters.official')} + {t('packages.filters.third_party')} @@ -158,9 +160,9 @@ export function PackageBrowser({ {/* Package List */} - Available Packages ({filteredPackages.length}) + {t('packages.browse')} ({filteredPackages.length}) - Select packages to include in your installation script + {t('packages.description')} @@ -219,7 +221,7 @@ export function PackageBrowser({ {filteredPackages.length === 0 && (
-

No packages found matching your criteria

+

{t('packages.no_packages')}

)} diff --git a/src/components/PlatformSelector.tsx b/src/components/PlatformSelector.tsx index ac06399..087db6e 100644 --- a/src/components/PlatformSelector.tsx +++ b/src/components/PlatformSelector.tsx @@ -4,6 +4,7 @@ import { useState } from 'react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { platforms } from '@/data/mockData' +import { useLocale } from '@/contexts/LocaleContext' import { Platform } from '@/types' interface PlatformSelectorProps { @@ -12,35 +13,37 @@ interface PlatformSelectorProps { } export function PlatformSelector({ selectedPlatform, onPlatformSelect }: PlatformSelectorProps) { + const { t } = useLocale() + return ( - - Select Your Platform - - Choose your operating system and package manager to browse available packages + + {t('platform.select')} + + {t('platform.description')} - -
+ +
{platforms.map((platform) => ( ))}
{selectedPlatform && ( -
-

- Selected: {selectedPlatform.name} ({selectedPlatform.packageManager}) +

+

+ {t('platform.selected')}: {selectedPlatform.name} ({selectedPlatform.packageManager})

)} diff --git a/src/components/RepoHubApp.tsx b/src/components/RepoHubApp.tsx index e571688..bbc1284 100644 --- a/src/components/RepoHubApp.tsx +++ b/src/components/RepoHubApp.tsx @@ -1,14 +1,18 @@ "use client" import { useState } from 'react' +import { LocaleProvider } from '@/contexts/LocaleContext' +import { Header } from '@/components/Header' import { PlatformSelector } from '@/components/PlatformSelector' import { PackageBrowser } from '@/components/PackageBrowser' import { SelectionManager } from '@/components/SelectionManager' import { ScriptPreview } from '@/components/ScriptPreview' import { generateScript } from '@/lib/scriptGenerator' +import { useLocale } from '@/contexts/LocaleContext' import { Platform, Package, SelectedPackage, FilterOptions, GeneratedScript } from '@/types' -export function RepoHubApp() { +function RepoHubAppContent() { + const { t, locale } = useLocale() const [selectedPlatform, setSelectedPlatform] = useState(null) const [selectedPackages, setSelectedPackages] = useState([]) const [generatedScript, setGeneratedScript] = useState(null) @@ -56,18 +60,20 @@ export function RepoHubApp() { } return ( -
+
+
+
{/* Header */}
-

+

RepoHub

-

- Cross-Platform Package Manager +

+ {t('common.subtitle')}

-

- Simplify software installation across Linux, Windows, and macOS with official repositories +

+ {t('common.description')}

@@ -109,3 +115,11 @@ export function RepoHubApp() {
) } + +export function RepoHubApp() { + return ( + + + + ) +} diff --git a/src/components/SelectionManager.tsx b/src/components/SelectionManager.tsx index 46e7289..66d379e 100644 --- a/src/components/SelectionManager.tsx +++ b/src/components/SelectionManager.tsx @@ -2,6 +2,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' +import { useLocale } from '@/contexts/LocaleContext' import { Package, SelectedPackage } from '@/types' import { X, PackageOpen, Download } from 'lucide-react' @@ -18,13 +19,15 @@ export function SelectionManager({ onClearAll, onGenerateScript }: SelectionManagerProps) { + const { t } = useLocale() + if (selectedPackages.length === 0) { return (
-

No packages selected

+

{t('selection.none')}

@@ -36,18 +39,18 @@ export function SelectionManager({
- Selected Packages ({selectedPackages.length}) + {t('selection.title')} ({selectedPackages.length}) - These packages will be included in your installation script + {t('selection.description')}
@@ -90,8 +93,7 @@ export function SelectionManager({

- Note: The generated script will use official repositories only - and will be idempotent (safe to run multiple times). + Note: {t('selection.note')}

diff --git a/src/contexts/LocaleContext.tsx b/src/contexts/LocaleContext.tsx new file mode 100644 index 0000000..341c221 --- /dev/null +++ b/src/contexts/LocaleContext.tsx @@ -0,0 +1,189 @@ +"use client" + +import { createContext, useContext, useState, useEffect, ReactNode } from 'react' + +type Locale = 'en' | 'tr' + +const translations = { + en: { + common: { + title: "RepoHub - Cross-Platform Package Manager", + subtitle: "Cross-Platform Package Manager", + description: "Simplify software installation across Linux, Windows, and macOS with official repositories" + }, + platform: { + select: "Select Your Platform", + description: "Choose your operating system and package manager to browse available packages", + selected: "Selected" + }, + packages: { + browse: "Available Packages", + description: "Select packages to include in your installation script", + search: "Search packages...", + no_packages: "No packages found matching your criteria", + filters: { + category: "Category", + type: "Type", + repository: "Repository", + gui: "GUI Applications", + cli: "CLI Tools", + official: "Official Only", + third_party: "Third Party" + } + }, + selection: { + title: "Selected Packages", + description: "These packages will be included in your installation script", + none: "No packages selected", + clear_all: "Clear All", + generate_script: "Generate Script", + note: "The generated script will use official repositories only and will be idempotent (safe to run multiple times)." + }, + script: { + title: "Installation Script", + description: "Idempotent script for", + official_repos: "Official Repositories", + official_repos_desc: "All packages from trusted sources", + idempotent: "Idempotent", + idempotent_desc: "Safe to run multiple times", + packages_count: "Packages", + packages_count_desc: "Ready for installation", + included_packages: "Included Packages:", + script_content: "Script Content:", + copy: "Copy", + copied: "Copied!", + download: "Download", + usage: "How to use:", + usage_steps: [ + "Download the script file to your target machine", + "Make it executable (for Linux/macOS):", + "Run the script with appropriate permissions", + "The script will automatically handle repository setup and package installation" + ] + } + }, + tr: { + common: { + title: "RepoHub - Çok Platformlu Paket Yöneticisi", + subtitle: "Çok Platformlu Paket Yöneticisi", + description: "Linux, Windows ve macOS'te resmi depoları kullanarak yazılım kurulumunu basitleştirin" + }, + platform: { + select: "Platformunuzu Seçin", + description: "Mevcut paketlere göz atmak için işletim sisteminizi ve paket yöneticinizi seçin", + selected: "Seçildi" + }, + packages: { + browse: "Mevcut Paketler", + description: "Kurulum scriptinize dahil edilecek paketleri seçin", + search: "Paket ara...", + no_packages: "Kriterlerinize uyan paket bulunamadı", + filters: { + category: "Kategori", + type: "Tür", + repository: "Depo", + gui: "GUI Uygulamaları", + cli: "CLI Araçları", + official: "Sadece Resmi", + third_party: "Üçüncü Parti" + } + }, + selection: { + title: "Seçilen Paketler", + description: "Bu paketler kurulum scriptinize dahil edilecek", + none: "Seçilen paket yok", + clear_all: "Tümünü Temizle", + generate_script: "Script Oluştur", + note: "Oluşturulan script yalnızca resmi depoları kullanacak ve idempotent olacak (birden çok kez çalıştırılması güvenli)." + }, + script: { + title: "Kurulum Scripti", + description: "için idempotent script", + official_repos: "Resmi Depolar", + official_repos_desc: "Tüm paketler güvenilir kaynaklardan", + idempotent: "Idempotent", + idempotent_desc: "Birden çok kez çalıştırılması güvenli", + packages_count: "Paket", + packages_count_desc: "Kurulum için hazır", + included_packages: "Dahil Edilen Paketler:", + script_content: "Script İçeriği:", + copy: "Kopyala", + copied: "Kopyalandı!", + download: "İndir", + usage: "Nasıl kullanılır:", + usage_steps: [ + "Script dosyasını hedef makinenize indirin", + "Çalıştırılabilir yapın (Linux/macOS için):", + "Scripti uygun izinlerle çalıştırın", + "Script otomatik olarak depo kurulumunu ve paket kurulumunu yönetecektir" + ] + } + } +} + +interface LocaleContextType { + locale: Locale + changeLocale: (locale: Locale) => void + toggleLocale: () => void + t: (key: string) => string +} + +const LocaleContext = createContext(undefined) + +export function LocaleProvider({ children }: { children: ReactNode }) { + const [locale, setLocale] = useState('en') + + useEffect(() => { + // Detect user's preferred locale + const detectLocale = (): Locale => { + const stored = localStorage.getItem('locale') as Locale + if (stored && (stored === 'en' || stored === 'tr')) { + return stored + } + + // Get browser language + const browserLang = navigator.language.toLowerCase() + if (browserLang.startsWith('tr')) { + return 'tr' + } + + return 'en' + } + + setLocale(detectLocale()) + }, []) + + const changeLocale = (newLocale: Locale) => { + setLocale(newLocale) + localStorage.setItem('locale', newLocale) + } + + const toggleLocale = () => { + changeLocale(locale === 'en' ? 'tr' : 'en') + } + + const t = (key: string) => { + const keys = key.split('.') + let value: any = translations[locale] + + for (const k of keys) { + value = value?.[k] + } + + return value || key + } + + return ( + + {children} + + ) +} + +export function useLocale() { + const context = useContext(LocaleContext) + if (context === undefined) { + throw new Error('useLocale must be used within a LocaleProvider') + } + return context +} diff --git a/src/hooks/useLocale.ts b/src/hooks/useLocale.ts new file mode 100644 index 0000000..94f3efa --- /dev/null +++ b/src/hooks/useLocale.ts @@ -0,0 +1,173 @@ +"use client" + +import { useState, useEffect } from 'react' + +type Locale = 'en' | 'tr' + +const translations = { + en: { + common: { + title: "RepoHub - Cross-Platform Package Manager", + subtitle: "Cross-Platform Package Manager", + description: "Simplify software installation across Linux, Windows, and macOS with official repositories" + }, + platform: { + select: "Select Your Platform", + description: "Choose your operating system and package manager to browse available packages", + selected: "Selected" + }, + packages: { + browse: "Available Packages", + description: "Select packages to include in your installation script", + search: "Search packages...", + no_packages: "No packages found matching your criteria", + filters: { + category: "Category", + type: "Type", + repository: "Repository", + gui: "GUI Applications", + cli: "CLI Tools", + official: "Official Only", + third_party: "Third Party" + } + }, + selection: { + title: "Selected Packages", + description: "These packages will be included in your installation script", + none: "No packages selected", + clear_all: "Clear All", + generate_script: "Generate Script", + note: "The generated script will use official repositories only and will be idempotent (safe to run multiple times)." + }, + script: { + title: "Installation Script", + description: "Idempotent script for", + official_repos: "Official Repositories", + official_repos_desc: "All packages from trusted sources", + idempotent: "Idempotent", + idempotent_desc: "Safe to run multiple times", + packages_count: "Packages", + packages_count_desc: "Ready for installation", + included_packages: "Included Packages:", + script_content: "Script Content:", + copy: "Copy", + copied: "Copied!", + download: "Download", + usage: "How to use:", + usage_steps: [ + "Download the script file to your target machine", + "Make it executable (for Linux/macOS):", + "Run the script with appropriate permissions", + "The script will automatically handle repository setup and package installation" + ] + } + }, + tr: { + common: { + title: "RepoHub - Çok Platformlu Paket Yöneticisi", + subtitle: "Çok Platformlu Paket Yöneticisi", + description: "Linux, Windows ve macOS'te resmi depoları kullanarak yazılım kurulumunu basitleştirin" + }, + platform: { + select: "Platformunuzu Seçin", + description: "Mevcut paketlere göz atmak için işletim sisteminizi ve paket yöneticinizi seçin", + selected: "Seçildi" + }, + packages: { + browse: "Mevcut Paketler", + description: "Kurulum scriptinize dahil edilecek paketleri seçin", + search: "Paket ara...", + no_packages: "Kriterlerinize uyan paket bulunamadı", + filters: { + category: "Kategori", + type: "Tür", + repository: "Depo", + gui: "GUI Uygulamaları", + cli: "CLI Araçları", + official: "Sadece Resmi", + third_party: "Üçüncü Parti" + } + }, + selection: { + title: "Seçilen Paketler", + description: "Bu paketler kurulum scriptinize dahil edilecek", + none: "Seçilen paket yok", + clear_all: "Tümünü Temizle", + generate_script: "Script Oluştur", + note: "Oluşturulan script yalnızca resmi depoları kullanacak ve idempotent olacak (birden çok kez çalıştırılması güvenli)." + }, + script: { + title: "Kurulum Scripti", + description: "için idempotent script", + official_repos: "Resmi Depolar", + official_repos_desc: "Tüm paketler güvenilir kaynaklardan", + idempotent: "Idempotent", + idempotent_desc: "Birden çok kez çalıştırılması güvenli", + packages_count: "Paket", + packages_count_desc: "Kurulum için hazır", + included_packages: "Dahil Edilen Paketler:", + script_content: "Script İçeriği:", + copy: "Kopyala", + copied: "Kopyalandı!", + download: "İndir", + usage: "Nasıl kullanılır:", + usage_steps: [ + "Script dosyasını hedef makinenize indirin", + "Çalıştırılabilir yapın (Linux/macOS için):", + "Scripti uygun izinlerle çalıştırın", + "Script otomatik olarak depo kurulumunu ve paket kurulumunu yönetecektir" + ] + } + } +} + +export function useLocale() { + const [locale, setLocale] = useState('en') + + useEffect(() => { + // Detect user's preferred locale + const detectLocale = (): Locale => { + const stored = localStorage.getItem('locale') as Locale + if (stored && (stored === 'en' || stored === 'tr')) { + return stored + } + + // Get browser language + const browserLang = navigator.language.toLowerCase() + if (browserLang.startsWith('tr')) { + return 'tr' + } + + return 'en' + } + + setLocale(detectLocale()) + }, []) + + const changeLocale = (newLocale: Locale) => { + setLocale(newLocale) + localStorage.setItem('locale', newLocale) + } + + const toggleLocale = () => { + changeLocale(locale === 'en' ? 'tr' : 'en') + } + + const t = (key: string) => { + const keys = key.split('.') + let value: any = translations[locale] + + for (const k of keys) { + value = value?.[k] + } + + return value || key + } + + return { + locale, + changeLocale, + toggleLocale, + t + } +} diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts new file mode 100644 index 0000000..ad69e0d --- /dev/null +++ b/src/hooks/useTheme.ts @@ -0,0 +1,65 @@ +"use client" + +import { useState, useEffect } from 'react' + +type Theme = 'light' | 'dark' | 'system' + +export function useTheme() { + const [theme, setTheme] = useState('system') + const [isDark, setIsDark] = useState(false) + + useEffect(() => { + // Get stored theme or default to system + const stored = localStorage.getItem('theme') as Theme + setTheme(stored || 'system') + }, []) + + useEffect(() => { + const updateTheme = () => { + if (theme === 'system') { + setIsDark(window.matchMedia('(prefers-color-scheme: dark)').matches) + } else { + setIsDark(theme === 'dark') + } + } + + updateTheme() + + if (theme === 'system') { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + mediaQuery.addEventListener('change', updateTheme) + return () => mediaQuery.removeEventListener('change', updateTheme) + } + }, [theme]) + + useEffect(() => { + // Apply theme to document + if (isDark) { + document.documentElement.classList.add('dark') + } else { + document.documentElement.classList.remove('dark') + } + }, [isDark]) + + const setThemeMode = (newTheme: Theme) => { + setTheme(newTheme) + localStorage.setItem('theme', newTheme) + } + + const toggleTheme = () => { + if (theme === 'light') { + setThemeMode('dark') + } else if (theme === 'dark') { + setThemeMode('light') + } else { + setThemeMode(isDark ? 'light' : 'dark') + } + } + + return { + theme, + isDark, + setThemeMode, + toggleTheme + } +}