Skip to content

Commit dedfb04

Browse files
Musiker15claude
andcommitted
feat(i18n): localize homepage, navbar and footer (EN/DE)
Make the landing surface bilingual via the existing global language switcher. Two new translation tables in lib/i18n.ts: homeTranslations (Hero, TrustBar, WhyMSK incl. feature copy, Featured/Custom headings, CTA) and layoutTranslations (navbar links + UI, footer). The homepage components are server components: app/page.tsx resolves the language from the cookie and passes `lang` down; the navbar switch's router.refresh() re-renders them in the new language. Header and Footer are client components and translate instantly via useLang(). Brand and product names (Ticket Bot, Giveaway Bot, Dashboard, ecosystem links) stay untranslated by design. content/home-features.ts is reduced to HOME_FEATURE_ICONS; the feature text now lives in homeTranslations and is zipped by index. Mobile: the language dropdown shows only the flag until lg, so the action bar does not crowd in the sm–md range. Out of scope (deliberately): the large ticketbot/giveaway landing pages and Tebex/custom package data. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e7c5c68 commit dedfb04

12 files changed

Lines changed: 283 additions & 126 deletions

File tree

app/page.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
1+
import { cookies, headers } from 'next/headers'
2+
import { LANG_COOKIE_NAME, resolveLang } from '@/lib/lang'
13
import { Hero } from '@/components/home/Hero'
24
import { TrustBar } from '@/components/home/TrustBar'
35
import { FeaturedPackages } from '@/components/home/FeaturedPackages'
46
import { WhyMSK } from '@/components/home/WhyMSK'
57
import { CustomPackages } from '@/components/home/CustomPackages'
68
import { CTASection } from '@/components/home/CTASection'
79

8-
export const revalidate = 60
9-
1010
export default async function HomePage() {
11+
const [cookieStore, headerStore] = await Promise.all([cookies(), headers()])
12+
const lang = resolveLang(cookieStore.get(LANG_COOKIE_NAME)?.value, headerStore.get('accept-language'))
13+
1114
return (
1215
<>
13-
<Hero />
14-
<TrustBar />
15-
<FeaturedPackages />
16-
<WhyMSK />
17-
<CustomPackages />
18-
<CTASection />
16+
<Hero lang={lang} />
17+
<TrustBar lang={lang} />
18+
<FeaturedPackages lang={lang} />
19+
<WhyMSK lang={lang} />
20+
<CustomPackages lang={lang} />
21+
<CTASection lang={lang} />
1922
</>
2023
)
2124
}

components/home/CTASection.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Sparkles, Github } from 'lucide-react'
22
import { Button } from '@/components/ui/Button'
3+
import { homeTranslations, type Lang } from '@/lib/i18n'
34

45
const DISCORD_URL = 'https://discord.gg/5hHSBRHvJE'
56
const GITHUB_URL = 'https://github.com/MSK-Scripts'
@@ -12,7 +13,8 @@ function DiscordLogo() {
1213
)
1314
}
1415

15-
export function CTASection() {
16+
export function CTASection({ lang }: { lang: Lang }) {
17+
const t = homeTranslations[lang]
1618
return (
1719
<section className="border-t border-[var(--color-border)] bg-[color-mix(in_oklab,var(--color-muted)_60%,var(--color-background))]">
1820
<div className="container-page py-16 md:py-24">
@@ -21,22 +23,22 @@ export function CTASection() {
2123
<Sparkles className="h-7 w-7 text-[var(--color-primary)]" aria-hidden="true" />
2224
</div>
2325
<h2 className="text-balance text-3xl font-bold tracking-tight md:text-4xl">
24-
Join 500+ server owners already running MSK.
26+
{t.cta_heading}
2527
</h2>
2628
<p className="mt-4 text-base text-[var(--color-muted-foreground)] md:text-lg">
27-
Discord support, sneak peeks, and announcements — straight from the developer.
29+
{t.cta_subtitle}
2830
</p>
2931
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
3032
<Button asChild size="lg" variant="discord">
3133
<a href={DISCORD_URL} target="_blank" rel="noopener noreferrer">
3234
<DiscordLogo />
33-
Join Discord
35+
{t.cta_btn_discord}
3436
</a>
3537
</Button>
3638
<Button asChild size="lg" variant="outline">
3739
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer">
3840
<Github className="h-4 w-4" />
39-
Browse GitHub
41+
{t.cta_btn_github}
4042
</a>
4143
</Button>
4244
</div>

components/home/CustomPackages.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { ExternalLink } from 'lucide-react'
33
import { Card, CardContent, CardTitle, CardDescription } from '@/components/ui/Card'
44
import { Badge, type BadgeVariant } from '@/components/ui/Badge'
55
import { Button } from '@/components/ui/Button'
6-
import { CUSTOM_PACKAGES, CUSTOM_PACKAGES_TITLE } from '@/content/custom-packages'
6+
import { CUSTOM_PACKAGES } from '@/content/custom-packages'
7+
import { homeTranslations, type Lang } from '@/lib/i18n'
78

89
/**
910
* Normalisiert Bildpfade aus custom-packages.ts. next/image akzeptiert nur
@@ -17,20 +18,21 @@ function resolveImageSrc(src: string): string {
1718
return `/${src}`
1819
}
1920

20-
export function CustomPackages() {
21+
export function CustomPackages({ lang }: { lang: Lang }) {
22+
const t = homeTranslations[lang]
2123
if (CUSTOM_PACKAGES.length === 0) {
2224
return null
2325
}
2426

2527
return (
2628
<section className="container-page py-16 md:py-20">
2729
<div className="mb-10">
28-
<span className="eyebrow">GitHub &amp; More</span>
30+
<span className="eyebrow">{t.custom_eyebrow}</span>
2931
<h2 className="mt-3 text-3xl font-bold tracking-tight md:text-4xl">
30-
{CUSTOM_PACKAGES_TITLE}
32+
{t.custom_title}
3133
</h2>
3234
<p className="mt-3 max-w-xl text-base text-[var(--color-muted-foreground)]">
33-
Free tools, Discord bots, and open-source libraries from the MSK ecosystem.
35+
{t.custom_subtitle}
3436
</p>
3537
</div>
3638

components/home/FeaturedPackages.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { ArrowRight } from 'lucide-react'
33
import { getPackages } from '@/lib/tebex'
44
import { PackageCard } from '@/components/packages/PackageCard'
55
import { FEATURED_PACKAGE_IDS, PACKAGE_BADGES, PACKAGE_TAGS, PACKAGE_DESCRIPTIONS } from '@/lib/config'
6+
import { homeTranslations, type Lang } from '@/lib/i18n'
67

7-
export async function FeaturedPackages() {
8+
export async function FeaturedPackages({ lang }: { lang: Lang }) {
9+
const t = homeTranslations[lang]
810
// Fail-soft: ist die Tebex-API zur Build-Zeit nicht erreichbar/autorisiert
911
// (z. B. CI-Builds ohne Secrets wie bei Dependabot-PRs), wird die Sektion
1012
// ausgeblendet statt den ganzen Build/Prerender zu sprengen.
@@ -20,7 +22,7 @@ export async function FeaturedPackages() {
2022
return null
2123
}
2224

23-
const heading = FEATURED_PACKAGE_IDS.length > 0 ? 'Featured Packages' : 'All Packages'
25+
const heading = FEATURED_PACKAGE_IDS.length > 0 ? t.featured_heading_featured : t.featured_heading_all
2426

2527
return (
2628
<section className="container-page py-16 md:py-20">
@@ -36,7 +38,7 @@ export async function FeaturedPackages() {
3638
prefetch={true}
3739
className="hidden items-center gap-1 text-sm font-medium text-[var(--color-primary)] transition-colors hover:underline sm:inline-flex"
3840
>
39-
All packages
41+
{t.featured_all_link}
4042
<ArrowRight className="h-3.5 w-3.5" />
4143
</Link>
4244
</div>

components/home/Hero.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import Link from 'next/link'
22
import { ArrowRight, Github } from 'lucide-react'
33
import { Button } from '@/components/ui/Button'
44
import { Badge } from '@/components/ui/Badge'
5+
import { homeTranslations, type Lang } from '@/lib/i18n'
56

6-
export function Hero() {
7+
export function Hero({ lang }: { lang: Lang }) {
8+
const t = homeTranslations[lang]
79
return (
810
<section className="relative overflow-hidden border-b border-[var(--color-border)]">
911
<div aria-hidden className="hero-decor-gradient pointer-events-none absolute inset-0" />
@@ -14,31 +16,30 @@ export function Hero() {
1416
<div>
1517
<Badge variant="outline" className="mb-6">
1618
<span className="pulse-dot inline-block h-1.5 w-1.5 rounded-full bg-[var(--color-primary)]" />
17-
Live · Trusted by 500+ Servers
19+
{t.hero_badge}
1820
</Badge>
1921

2022
<h1 className="text-balance text-4xl font-bold tracking-tight md:text-5xl lg:text-6xl">
21-
Premium FiveM<br />
22-
Scripts.<br />
23-
<span className="text-[var(--color-primary)]">Built by a player.</span>
23+
{t.hero_h1_line1}<br />
24+
{t.hero_h1_line2}<br />
25+
<span className="text-[var(--color-primary)]">{t.hero_h1_accent}</span>
2426
</h1>
2527

2628
<p className="mt-6 max-w-xl text-pretty text-lg text-[var(--color-muted-foreground)] md:text-xl">
27-
Clean code, regular updates, and real support — from the developer
28-
who codes every line himself.
29+
{t.hero_subtitle}
2930
</p>
3031

3132
<div className="mt-8 flex flex-wrap gap-3">
3233
<Button asChild size="lg">
3334
<Link href="/packages">
34-
Browse Packages
35+
{t.hero_btn_browse}
3536
<ArrowRight className="h-4 w-4" />
3637
</Link>
3738
</Button>
3839
<Button asChild size="lg" variant="outline">
3940
<a href="https://github.com/MSK-Scripts" target="_blank" rel="noopener noreferrer">
4041
<Github className="h-4 w-4" />
41-
View on GitHub
42+
{t.hero_btn_github}
4243
</a>
4344
</Button>
4445
</div>

components/home/TrustBar.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1+
import { homeTranslations, type Lang } from '@/lib/i18n'
2+
13
interface Kpi {
24
number: string
35
label: string
46
emphasis?: string
57
}
68

7-
const KPIS: Kpi[] = [
8-
{ number: '500', emphasis: '+', label: 'Customers' },
9-
{ number: '20', emphasis: '+', label: 'Resources' },
10-
{ number: '24/7', label: 'Discord Support' },
11-
{ number: 'ESX · QB', label: 'Frameworks' },
12-
]
13-
14-
export function TrustBar() {
9+
export function TrustBar({ lang }: { lang: Lang }) {
10+
const t = homeTranslations[lang]
11+
const KPIS: Kpi[] = [
12+
{ number: '500', emphasis: '+', label: t.trust_customers },
13+
{ number: '20', emphasis: '+', label: t.trust_resources },
14+
{ number: '24/7', label: t.trust_support },
15+
{ number: 'ESX · QB', label: t.trust_frameworks },
16+
]
1517
return (
1618
<section className="border-b border-[var(--color-border)] bg-[color-mix(in_oklab,var(--color-muted)_60%,var(--color-background))]">
1719
<div className="container-page grid grid-cols-2 gap-6 py-10 md:grid-cols-4 md:py-12">

components/home/WhyMSK.tsx

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,37 @@
11
import { Card } from '@/components/ui/Card'
2-
import { HOME_FEATURES } from '@/content/home-features'
2+
import { HOME_FEATURE_ICONS } from '@/content/home-features'
3+
import { homeTranslations, type Lang } from '@/lib/i18n'
34

4-
export function WhyMSK() {
5+
export function WhyMSK({ lang }: { lang: Lang }) {
6+
const t = homeTranslations[lang]
57
return (
68
<section className="border-b border-t border-[var(--color-border)] bg-[color-mix(in_oklab,var(--color-muted)_40%,var(--color-background))]">
79
<div className="container-page py-16 md:py-20">
810
<div className="mx-auto mb-12 max-w-2xl text-center">
9-
<span className="eyebrow mx-auto inline-flex">Benefits</span>
11+
<span className="eyebrow mx-auto inline-flex">{t.why_eyebrow}</span>
1012
<h2 className="mt-3 text-3xl font-bold tracking-tight md:text-4xl">
11-
Why MSK Scripts?
13+
{t.why_heading}
1214
</h2>
1315
<p className="mt-4 text-base text-[var(--color-muted-foreground)]">
14-
No template farms. No corporate roadmap. Just well-crafted FiveM
15-
scripts you can trust.
16+
{t.why_subtitle}
1617
</p>
1718
</div>
1819

1920
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
20-
{HOME_FEATURES.map(({ icon: Icon, title, description }) => (
21-
<Card key={title} hoverLift className="group p-6">
22-
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl bg-[color-mix(in_oklab,var(--color-primary)_12%,transparent)] text-[var(--color-primary)] transition-transform duration-200 group-hover:scale-105">
23-
<Icon className="h-5 w-5" />
24-
</div>
25-
<h3 className="mb-2 text-base font-bold">{title}</h3>
26-
<p className="text-sm leading-relaxed text-[var(--color-muted-foreground)]">
27-
{description}
28-
</p>
29-
</Card>
30-
))}
21+
{HOME_FEATURE_ICONS.map((Icon, i) => {
22+
const f = t.why_features[i]
23+
return (
24+
<Card key={i} hoverLift className="group p-6">
25+
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl bg-[color-mix(in_oklab,var(--color-primary)_12%,transparent)] text-[var(--color-primary)] transition-transform duration-200 group-hover:scale-105">
26+
<Icon className="h-5 w-5" />
27+
</div>
28+
<h3 className="mb-2 text-base font-bold">{f.title}</h3>
29+
<p className="text-sm leading-relaxed text-[var(--color-muted-foreground)]">
30+
{f.description}
31+
</p>
32+
</Card>
33+
)
34+
})}
3135
</div>
3236
</div>
3337
</section>

components/i18n/LanguageDropdown.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ export function LanguageDropdown() {
2828
aria-label="Change language"
2929
>
3030
<span aria-hidden>{current.flag}</span>
31-
<span className="hidden font-medium sm:inline">{current.label}</span>
31+
{/* Label erst ab lg — auf Mobile/Tablet kompakt (nur Flagge), damit die
32+
Action-Leiste im sm–md-Bereich nicht überläuft. */}
33+
<span className="hidden font-medium lg:inline">{current.label}</span>
3234
<ChevronDown className={cn('h-3 w-3 transition-transform', open && 'rotate-180')} />
3335
</Button>
3436

components/layout/Footer.tsx

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,14 @@ import Image from 'next/image'
55
import { Github } from 'lucide-react'
66
import { Button } from '@/components/ui/Button'
77
import { useLang } from '@/components/i18n/LangProvider'
8-
9-
const SHOP_LINKS = [
10-
{ label: 'All Packages', href: '/packages' },
11-
{ label: 'Github', href: 'https://github.com/MSK-Scripts', external: true },
12-
{ label: 'Documentation', href: 'https://docu.msk-scripts.de/', external: true },
13-
] as const
8+
import { layoutTranslations } from '@/lib/i18n'
149

1510
const ECOSYSTEM_LINKS = [
1611
{ label: 'MSKanban', href: 'https://mskanban.msk-scripts.de/', external: true },
1712
{ label: 'MSK Paste', href: 'https://paste.msk-scripts.de/', external: true },
1813
{ label: 'MSK Shortener', href: 'https://s.msk-scripts.de/', external: true },
1914
] as const
2015

21-
// Legal-Labels sind sprachabhängig (z. B. Imprint ⇄ Impressum).
22-
const LEGAL_LINKS = [
23-
{ en: 'Imprint', de: 'Impressum', href: '/terms/imprint' },
24-
{ en: 'Privacy Policy', de: 'Datenschutz', href: '/terms/privacy' },
25-
{ en: 'Terms & Conditions', de: 'AGB', href: '/terms' },
26-
] as const
27-
2816
const DISCORD_URL = 'https://discord.gg/5hHSBRHvJE'
2917
const GITHUB_URL = 'https://github.com/MSK-Scripts'
3018

@@ -49,6 +37,20 @@ function ColumnTitle({ children }: { children: React.ReactNode }) {
4937

5038
export function Footer() {
5139
const { lang } = useLang()
40+
const t = layoutTranslations[lang]
41+
42+
const shopLinks = [
43+
{ label: t.footer_all_packages, href: '/packages' },
44+
{ label: 'Github', href: GITHUB_URL, external: true },
45+
{ label: t.footer_documentation, href: 'https://docu.msk-scripts.de/', external: true },
46+
]
47+
// Legal-Labels sind sprachabhängig (z. B. Imprint ⇄ Impressum).
48+
const legalLinks = [
49+
{ label: t.legal_imprint, href: '/terms/imprint' },
50+
{ label: t.legal_privacy, href: '/terms/privacy' },
51+
{ label: t.legal_terms, href: '/terms' },
52+
]
53+
5254
return (
5355
<footer className="mt-16 border-t border-[var(--color-border)] bg-[color-mix(in_oklab,var(--color-muted)_40%,var(--color-background))]">
5456
<div className="container-page py-12">
@@ -61,7 +63,7 @@ export function Footer() {
6163
<span>MSK Scripts</span>
6264
</Link>
6365
<p className="mt-3 max-w-xs text-sm text-[var(--color-muted-foreground)]">
64-
FiveM Scripts with Heart 💚
66+
{t.footer_tagline}
6567
</p>
6668
<div className="mt-4 flex gap-2">
6769
<Button asChild variant="outline" size="sm" aria-label="Discord">
@@ -84,25 +86,25 @@ export function Footer() {
8486

8587
{/* Shop */}
8688
<div>
87-
<ColumnTitle>Shop</ColumnTitle>
88-
{SHOP_LINKS.map(l => (
89-
<FooterLink key={l.href} href={l.href}>{l.label}</FooterLink>
89+
<ColumnTitle>{t.footer_col_shop}</ColumnTitle>
90+
{shopLinks.map(l => (
91+
<FooterLink key={l.href} href={l.href} external={l.external}>{l.label}</FooterLink>
9092
))}
9193
</div>
9294

9395
{/* Ecosystem */}
9496
<div>
95-
<ColumnTitle>Ecosystem</ColumnTitle>
97+
<ColumnTitle>{t.footer_col_eco}</ColumnTitle>
9698
{ECOSYSTEM_LINKS.map(l => (
9799
<FooterLink key={l.href} href={l.href} external>{l.label}</FooterLink>
98100
))}
99101
</div>
100102

101103
{/* Legal */}
102104
<div>
103-
<ColumnTitle>{lang === 'de' ? 'Rechtliches' : 'Legal'}</ColumnTitle>
104-
{LEGAL_LINKS.map(l => (
105-
<FooterLink key={l.href} href={l.href}>{lang === 'de' ? l.de : l.en}</FooterLink>
105+
<ColumnTitle>{t.footer_col_legal}</ColumnTitle>
106+
{legalLinks.map(l => (
107+
<FooterLink key={l.href} href={l.href}>{l.label}</FooterLink>
106108
))}
107109
</div>
108110

@@ -111,15 +113,15 @@ export function Footer() {
111113

112114
<div className="mt-10 flex flex-wrap items-center justify-between gap-3 border-t border-[var(--color-border)] pt-6">
113115
<p className="text-xs text-[var(--color-muted-foreground)]">
114-
© {new Date().getFullYear()} MSK Scripts. All rights reserved.
116+
© {new Date().getFullYear()} MSK Scripts. {t.footer_rights}
115117
</p>
116118
<p className="text-xs text-[var(--color-muted-foreground)]">
117-
Powered by{' '}
119+
{t.footer_powered_by}{' '}
118120
<a href="https://tebex.io" target="_blank" rel="noopener noreferrer" className="hover:text-[var(--color-foreground)] transition-colors">
119121
Tebex
120122
</a>
121123
{' · '}
122-
Built by{' '}
124+
{t.footer_built_by}{' '}
123125
<a href="https://www.musiker15.de" target="_blank" rel="noopener noreferrer" className="hover:text-[var(--color-foreground)] transition-colors">
124126
Musiker15
125127
</a>

0 commit comments

Comments
 (0)