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
This commit is contained in:
Yusuf İpek
2025-11-10 17:44:04 +03:00
parent bb854ea4d0
commit e82366e5e8
8 changed files with 563 additions and 39 deletions
+76
View File
@@ -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 <Sun className="h-4 w-4" />
case 'dark':
return <Moon className="h-4 w-4" />
default:
return <Monitor className="h-4 w-4" />
}
}
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 (
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-14 items-center justify-between">
<div className="flex items-center space-x-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<span className="text-sm font-bold">RH</span>
</div>
<span className="hidden font-bold sm:inline-block">
RepoHub
</span>
</div>
<div className="flex items-center space-x-2">
{/* Theme Toggle */}
<Button
variant="ghost"
size="sm"
onClick={toggleTheme}
className="w-full justify-start"
>
{getThemeIcon()}
<span className="ml-2 hidden sm:inline">
{getThemeLabel()}
</span>
</Button>
{/* Language Toggle */}
<Button
variant="ghost"
size="sm"
onClick={toggleLocale}
className="w-full justify-start"
>
<Globe className="h-4 w-4" />
<span className="ml-2 hidden sm:inline">
{locale === 'tr' ? 'TR' : 'EN'}
</span>
</Button>
</div>
</div>
</header>
)
}
+14 -12
View File
@@ -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<FilterOptions>({
platforms: [],
@@ -83,7 +85,7 @@ export function PackageBrowser({
return (
<Card className="w-full">
<CardContent className="flex items-center justify-center h-64">
<p className="text-muted-foreground">Please select a platform first</p>
<p className="text-muted-foreground">{t('platform.description')}</p>
</CardContent>
</Card>
)
@@ -103,7 +105,7 @@ export function PackageBrowser({
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<input
type="text"
placeholder="Search packages..."
placeholder={t('packages.search')}
className="w-full pl-10 pr-4 py-2 border border-input rounded-md bg-background"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
@@ -115,7 +117,7 @@ export function PackageBrowser({
handleFilterChange('categories', value ? [value] : [])
}>
<SelectTrigger>
<SelectValue placeholder="Category" />
<SelectValue placeholder={t('packages.filters.category')} />
</SelectTrigger>
<SelectContent>
{categories.map(category => (
@@ -131,11 +133,11 @@ export function PackageBrowser({
handleFilterChange('types', value ? [value as 'gui' | 'cli'] : [])
}>
<SelectTrigger>
<SelectValue placeholder="Type" />
<SelectValue placeholder={t('packages.filters.type')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="gui">GUI Applications</SelectItem>
<SelectItem value="cli">CLI Tools</SelectItem>
<SelectItem value="gui">{t('packages.filters.gui')}</SelectItem>
<SelectItem value="cli">{t('packages.filters.cli')}</SelectItem>
</SelectContent>
</Select>
@@ -144,11 +146,11 @@ export function PackageBrowser({
handleFilterChange('repositories', value ? [value as 'official' | 'third-party'] : [])
}>
<SelectTrigger>
<SelectValue placeholder="Repository" />
<SelectValue placeholder={t('packages.filters.repository')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="official">Official Only</SelectItem>
<SelectItem value="third-party">Third Party</SelectItem>
<SelectItem value="official">{t('packages.filters.official')}</SelectItem>
<SelectItem value="third_party">{t('packages.filters.third_party')}</SelectItem>
</SelectContent>
</Select>
</div>
@@ -158,9 +160,9 @@ export function PackageBrowser({
{/* Package List */}
<Card>
<CardHeader>
<CardTitle>Available Packages ({filteredPackages.length})</CardTitle>
<CardTitle>{t('packages.browse')} ({filteredPackages.length})</CardTitle>
<CardDescription>
Select packages to include in your installation script
{t('packages.description')}
</CardDescription>
</CardHeader>
<CardContent>
@@ -219,7 +221,7 @@ export function PackageBrowser({
{filteredPackages.length === 0 && (
<div className="text-center py-8">
<PackageIcon className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<p className="text-muted-foreground">No packages found matching your criteria</p>
<p className="text-muted-foreground">{t('packages.no_packages')}</p>
</div>
)}
</div>
+16 -13
View File
@@ -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 (
<Card className="w-full">
<CardHeader>
<CardTitle>Select Your Platform</CardTitle>
<CardDescription>
Choose your operating system and package manager to browse available packages
<CardHeader className="pb-4">
<CardTitle className="text-lg">{t('platform.select')}</CardTitle>
<CardDescription className="text-sm">
{t('platform.description')}
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<CardContent className="pt-0">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3">
{platforms.map((platform) => (
<Button
key={platform.id}
variant={selectedPlatform?.id === platform.id ? "default" : "outline"}
className="h-auto p-4 flex flex-col items-center space-y-2"
className="h-auto p-3 flex flex-col items-center space-y-1"
onClick={() => onPlatformSelect(platform)}
>
<div className="text-2xl">{platform.icon}</div>
<div className="text-xl">{platform.icon}</div>
<div className="text-center">
<div className="font-semibold">{platform.name}</div>
<div className="text-sm text-muted-foreground">{platform.packageManager}</div>
<div className="font-semibold text-sm">{platform.name}</div>
<div className="text-xs text-muted-foreground">{platform.packageManager}</div>
</div>
</Button>
))}
</div>
{selectedPlatform && (
<div className="mt-4 p-4 bg-secondary rounded-md">
<p className="text-sm">
<strong>Selected:</strong> {selectedPlatform.name} ({selectedPlatform.packageManager})
<div className="mt-3 p-3 bg-secondary rounded-md">
<p className="text-xs">
<strong>{t('platform.selected')}:</strong> {selectedPlatform.name} ({selectedPlatform.packageManager})
</p>
</div>
)}
+21 -7
View File
@@ -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<Platform | null>(null)
const [selectedPackages, setSelectedPackages] = useState<SelectedPackage[]>([])
const [generatedScript, setGeneratedScript] = useState<GeneratedScript | null>(null)
@@ -56,18 +60,20 @@ export function RepoHubApp() {
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<div key={locale} className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800">
<Header />
<div className="container mx-auto px-4 py-8">
{/* Header */}
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-4">
RepoHub
</h1>
<p className="text-xl text-gray-600 mb-2">
Cross-Platform Package Manager
<p className="text-xl text-gray-600 dark:text-gray-300 mb-2">
{t('common.subtitle')}
</p>
<p className="text-lg text-gray-500 max-w-2xl mx-auto">
Simplify software installation across Linux, Windows, and macOS with official repositories
<p className="text-lg text-gray-500 dark:text-gray-400 max-w-2xl mx-auto">
{t('common.description')}
</p>
</div>
@@ -109,3 +115,11 @@ export function RepoHubApp() {
</div>
)
}
export function RepoHubApp() {
return (
<LocaleProvider>
<RepoHubAppContent />
</LocaleProvider>
)
}
+9 -7
View File
@@ -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 (
<Card className="w-full">
<CardContent className="flex items-center justify-center h-32">
<div className="text-center">
<PackageOpen className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
<p className="text-muted-foreground">No packages selected</p>
<p className="text-muted-foreground">{t('selection.none')}</p>
</div>
</CardContent>
</Card>
@@ -36,18 +39,18 @@ export function SelectionManager({
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Selected Packages ({selectedPackages.length})</CardTitle>
<CardTitle>{t('selection.title')} ({selectedPackages.length})</CardTitle>
<CardDescription>
These packages will be included in your installation script
{t('selection.description')}
</CardDescription>
</div>
<div className="flex space-x-2">
<Button variant="outline" size="sm" onClick={onClearAll}>
Clear All
{t('selection.clear_all')}
</Button>
<Button onClick={onGenerateScript}>
<Download className="h-4 w-4 mr-2" />
Generate Script
{t('selection.generate_script')}
</Button>
</div>
</div>
@@ -90,8 +93,7 @@ export function SelectionManager({
</div>
<div className="mt-4 p-3 bg-muted rounded-lg">
<p className="text-sm text-muted-foreground">
<strong>Note:</strong> The generated script will use official repositories only
and will be idempotent (safe to run multiple times).
<strong>Note:</strong> {t('selection.note')}
</p>
</div>
</CardContent>
+189
View File
@@ -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<LocaleContextType | undefined>(undefined)
export function LocaleProvider({ children }: { children: ReactNode }) {
const [locale, setLocale] = useState<Locale>('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 (
<LocaleContext.Provider value={{ locale, changeLocale, toggleLocale, t }}>
{children}
</LocaleContext.Provider>
)
}
export function useLocale() {
const context = useContext(LocaleContext)
if (context === undefined) {
throw new Error('useLocale must be used within a LocaleProvider')
}
return context
}
+173
View File
@@ -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<Locale>('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
}
}
+65
View File
@@ -0,0 +1,65 @@
"use client"
import { useState, useEffect } from 'react'
type Theme = 'light' | 'dark' | 'system'
export function useTheme() {
const [theme, setTheme] = useState<Theme>('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
}
}