Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2a598d4
feat: Add i18n framework support
asdofen Nov 16, 2025
ddc7a38
Merge pull request #25 from asdofen/feature/i18n-translation
richardscull Dec 2, 2025
111be3f
feat: Add cookies and save state to change locale
richardscull Dec 2, 2025
b3e872b
feat: Use router to refresh page on translation change and fallback t…
richardscull Dec 4, 2025
326c1db
feat: Localise main page
richardscull Dec 4, 2025
07fb336
feat: Add context for locale messages
richardscull Dec 20, 2025
8d3d7a1
feat: Create useT hook which extends useTranslations with custom defa…
richardscull Dec 20, 2025
50961fa
chore: Update naming convention for locales
richardscull Dec 20, 2025
e353bed
chore: use useT instead of useTranslations
richardscull Dec 20, 2025
3236483
fix!: Disable prefetching for links to fix missing metadata on titles
richardscull Dec 20, 2025
326084a
chore: Update next to latest stable version
richardscull Dec 20, 2025
722268c
feat: Localise main page metadata
richardscull Dec 20, 2025
230ba3c
feat: Localise wiki page
richardscull Dec 20, 2025
1d08d50
feat: Localise rules page
richardscull Dec 20, 2025
0d856b2
fix: Use legacy tags for wiki
richardscull Dec 20, 2025
7ce9267
feat: Localise register page
richardscull Dec 20, 2025
358da2f
feat: Localise support page
richardscull Dec 20, 2025
d9977ca
feat: Localise topplays page
richardscull Dec 21, 2025
99ac371
feat: Localise score page
richardscull Dec 21, 2025
4f403e5
chore: Set timezone to UTC
richardscull Dec 21, 2025
8f10ae9
feat: Localise leaderboard page
richardscull Dec 21, 2025
830b42a
feat: Localise friends page
richardscull Dec 21, 2025
76a4447
feat: Localise beatmaps page
richardscull Dec 21, 2025
640cbf3
feat: Localise beatmapsets page
richardscull Dec 21, 2025
5cd13e4
feat: Localise settings page
richardscull Dec 21, 2025
3fc8d64
feat: Localise user page
richardscull Dec 21, 2025
d223c08
feat: Add localisation for components
richardscull Dec 21, 2025
6253832
feat: Add LanguageSelector and deprecate temporary one
richardscull Dec 21, 2025
6e8ccc0
feat: Add custom language
richardscull Dec 21, 2025
f4f4361
feat: Add even more supported languages
richardscull Dec 21, 2025
e5cd003
feat: Add fallback font for cyrillic
richardscull Dec 21, 2025
3e0a455
Merge branch 'main' into feature/i18n-translation
richardscull Dec 21, 2025
5dc1957
chore: small fixes
richardscull Dec 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/(admin)/admin/beatmaps/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import RoundedContent from "@/components/General/RoundedContent";
import Image from "next/image";
import PrettyHeader from "@/components/General/PrettyHeader";
import { Music2 } from "lucide-react";
import { tryParseNumber } from "@/lib/utils/type.util";

interface BeatmapsProps {
params: Promise<{ id: number }>;
params: Promise<{ id: string }>;
}

export default function BeatmapsRedirect(props: BeatmapsProps) {
const params = use(props.params);
const beatmapQuery = useBeatmap(params.id);
const beatmapQuery = useBeatmap(tryParseNumber(params.id) ?? 0);
const beatmap = beatmapQuery.data;

if (beatmap) {
Expand Down
6 changes: 4 additions & 2 deletions app/(admin)/admin/beatmapsets/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,17 @@ import { BeatmapsStatusTable } from "@/app/(admin)/admin/beatmapsets/components/
import { BeatmapSetEvents } from "@/app/(admin)/admin/beatmapsets/components/BeatmapSetEvents";
import PrettyHeader from "@/components/General/PrettyHeader";
import Link from "next/link";
import { tryParseNumber } from "@/lib/utils/type.util";

export interface BeatmapsetProps {
params: Promise<{ id: number }>;
params: Promise<{ id: string }>;
}

export default function AdminBeatmapset(props: BeatmapsetProps) {
const params = use(props.params);
const router = useRouter();

const beatmapSetId = params.id;
const beatmapSetId = tryParseNumber(params.id) ?? 0;

const beatmapsetQuery = useBeatmapSet(beatmapSetId);
const beatmapSet = beatmapsetQuery.data;
Expand Down Expand Up @@ -120,6 +121,7 @@ export default function AdminBeatmapset(props: BeatmapsetProps) {
<Button variant="secondary" size="xl" asChild>
<Link
href={`https://osu.ppy.sh/beatmapsets/${beatmapSet.id}`}

>
<ExternalLink />
Open on Bancho
Expand Down
2 changes: 1 addition & 1 deletion app/(admin)/admin/users/[id]/edit/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { UserResponse } from "@/lib/types/api";
export const revalidate = 60;

export async function generateMetadata(props: {
params: Promise<{ id: number }>;
params: Promise<{ id: string }>;
}): Promise<Metadata> {
const params = await props.params;
const user = await fetcher<UserResponse>(`user/${params.id}`);
Expand Down
19 changes: 11 additions & 8 deletions app/(website)/(site)/components/ServerStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import PrettyCounter from "@/components/General/PrettyCounter";
import { Skeleton } from "@/components/ui/skeleton";
import { useT } from "@/lib/i18n/utils";
import { Activity, Users, AlertTriangle, Trophy, Wifi } from "lucide-react";

interface Props {
Expand All @@ -15,40 +16,42 @@ interface Props {

const statuses = {
total_users: {
name: "Total Users",
nameKey: "totalUsers",
icon: <Users className="h-4 w-4 text-blue-500" />,
},
users_online: {
name: "Users Online",
nameKey: "usersOnline",
icon: <Activity className="h-4 w-4 text-orange-500" />,
},
users_restricted: {
name: "Users Restricted",
nameKey: "usersRestricted",
icon: <AlertTriangle className="h-4 w-4 text-red-500" />,
},
total_scores: {
name: "Total Scores",
nameKey: "totalScores",
icon: <Trophy className="h-4 w-4 text-yellow-500" />,
},
server_status: {
name: "Server Status",
nameKey: "serverStatus",
icon: <Wifi className="h-4 w-4 " />,
},
};

export default function ServerStatus({ type, data, children }: Props) {
const isDataNumber = !isNaN(Number(data));

const t = useT("pages.mainPage.statuses");

return (
<div
className={`flex items-center gap-3 px-4 py-2 rounded-full bg-card border shadow`}
>
<div
className={`flex-shrink-0 ${
type === "server_status"
? data === "Online"
? data === t("online")
? "text-green-500"
: data === "Under Maintenance"
: data === t("underMaintenance")
? "text-orange-500"
: "text-red-500"
: "text-current"
Expand All @@ -59,7 +62,7 @@ export default function ServerStatus({ type, data, children }: Props) {

<div className="flex flex-col">
<span className="text-xs text-muted-foreground">
{statuses[type].name}
{t(statuses[type].nameKey)}
</span>
<div className="flex items-center gap-2">
<span className="text-lg font-bold text-current">
Expand Down
14 changes: 8 additions & 6 deletions app/(website)/(site)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Metadata } from "next";
import Page from "./page";
import { getT } from "@/lib/i18n/utils";

export const metadata: Metadata = {
title: "Welcome | osu!sunrise",
openGraph: {
title: "Welcome | osu!sunrise",
},
};
export async function generateMetadata(): Promise<Metadata> {
const t = await getT("pages.mainPage.meta");
return {
title: t("title"),
description: t("description"),
};
}

export default Page;
105 changes: 56 additions & 49 deletions app/(website)/(site)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,19 @@ import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { twMerge } from "tailwind-merge";
import { useT } from "@/lib/i18n/utils";
import { useTranslations } from "next-intl";

const cards = [
{
title: "Truly Free Features",
description:
"Enjoy features like osu!direct and username changes without any paywalls — completely free for all players!",
titleKey: "cards.freeFeatures.title",
descriptionKey: "cards.freeFeatures.description",
imageUrl: "/images/frontpage/freefeatures.png",
},

{
title: "Custom PP Calculations",
description:
"We use the latest performance point (PP) system for vanilla scores while applying a custom, well-balanced formula for Relax and Autopilot modes.",
titleKey: "cards.ppSystem.title",
descriptionKey: "cards.ppSystem.description",
imageUrl: "/images/frontpage/ppsystem.png",
},
// TODO: Soon™...
Expand All @@ -42,27 +43,23 @@ const cards = [
// imageUrl: "/images/not-found.jpg",
// },
{
title: "Earn Custom Medals",
description:
"Earn unique, server-exclusive medals as you accomplish various milestones and achievements.",
titleKey: "cards.medals.title",
descriptionKey: "cards.medals.description",
imageUrl: "/images/frontpage/medals.png",
},
{
title: "Frequent Updates",
description:
"We’re always improving! Expect regular updates, new features, and ongoing performance optimizations.",
titleKey: "cards.updates.title",
descriptionKey: "cards.updates.description",
imageUrl: "/images/frontpage/updates.png",
},
{
title: "Built-in PP Calculator",
description:
"Our website offers a built-in PP calculator for quick and easy performance point estimates.",
titleKey: "cards.ppCalc.title",
descriptionKey: "cards.ppCalc.description",
imageUrl: "/images/frontpage/ppcalc.png",
},
{
title: "Custom-Built Bancho Core",
description:
"Unlike most private osu! servers, we’ve developed our own custom bancho core for better stability and unique feature support.",
titleKey: "cards.sunriseCore.title",
descriptionKey: "cards.sunriseCore.description",
imageUrl: "/images/frontpage/sunrisecore.png",
},
];
Expand All @@ -73,6 +70,9 @@ export default function Home() {
boolean | null
>(null);

const t = useT("pages.mainPage");
const tGeneral = useT("general");

const serverStatusQuery = useServerStatus();
const serverStatus = serverStatusQuery.data;

Expand All @@ -93,28 +93,30 @@ export default function Home() {
<div className="flex flex-col justify-center space-y-4 my-4 md:w-5/12 ">
<div className="">
<h1 className="text-6xl">
<span className="text-primary dark">sun</span>
<span className="text-current">rise</span>
<span className="text-primary dark">
{tGeneral("serverTitle.split.part1")}
</span>
<span className="text-current">
{tGeneral("serverTitle.split.part2")}
</span>
</h1>
<p className="text-muted-foreground italic text-sm">
- yet another osu! server
{t("features.motto")}
</p>
</div>
<p>
Features rich osu! server with support for Relax, Autopilot and
ScoreV2 gameplay, with a custom art‑state PP calculation system
tailored for Relax and Autopilot.
</p>
<p>{t("features.description")}</p>
<div className="flex items-end space-x-4">
<Button
className="from-red-400 via-orange-400 to-yellow-400 bg-gradient-to-r bg-size-300 animate-gradient hover:scale-105 smooth-transition"
size="lg"
asChild
>
<Link href="/register">Join now</Link>
<Link href="/register">{t("features.buttons.register")}</Link>
</Button>
<Button variant="secondary" size="sm" asChild>
<Link href="/wiki#How%20to%20connect">How to connect</Link>
<Link href="/wiki#How%20to%20connect">
{t("features.buttons.wiki")}
</Link>
</Button>
</div>
</div>
Expand Down Expand Up @@ -148,9 +150,9 @@ export default function Home() {
serverStatus
? serverStatus.is_online
? serverStatus.is_on_maintenance
? "Under Maintenance"
: "Online"
: "Offline"
? t("statuses.underMaintenance")
: t("statuses.online")
: t("statuses.offline")
: undefined
}
/>
Expand All @@ -172,7 +174,7 @@ export default function Home() {
</div>

<div className="w-full pb-12 items-center">
<h2 className="text-4xl font-bold text-center py-8">Why us?</h2>
<h2 className="text-4xl font-bold text-center py-8">{t("whyUs")}</h2>

<Carousel className="w-full">
<CarouselContent className="-ml-1">
Expand All @@ -186,15 +188,17 @@ export default function Home() {
<div className="relative h-1/2 w-full">
<Image
src={card.imageUrl || "/placeholder.svg"}
alt={card.title}
alt={card.titleKey}
fill
className="object-cover rounded-t-lg "
/>
</div>
<CardContent className="p-4">
<h3 className="text-lg font-bold mb-2">{card.title}</h3>
<h3 className="text-lg font-bold mb-2">
{t(card.titleKey)}
</h3>
<p className="text-sm text-muted-foreground">
{card.description}
{t(card.descriptionKey)}
</p>
</CardContent>
</Card>
Expand All @@ -210,52 +214,55 @@ export default function Home() {
<div className="w-full p-4">
<div className="py-8 space-y-4">
<h2 className="text-4xl font-bold text-current">
How do I start playing?
{t("howToStart.title")}
</h2>

<p className="text-muted-foreground">
Just three simple steps and you're ready to go!
</p>
<p className="text-muted-foreground">{t("howToStart.description")}</p>
</div>

<div className="space-y-2">
<PrettyHeader icon={<Download />} className="rounded-lg">
<div className="flex flex-col md:flex-row space-y-2 w-full">
<div className="w-full flex flex-col mx-2">
<p className="text-lg">Download osu! client</p>
<p className="text-lg">{t("howToStart.downloadTile.title")}</p>
<p className="text-muted-foreground text-sm">
If you do not already have an installed client
{t("howToStart.downloadTile.description")}
</p>
</div>
<Button className="md:w-1/3 md:m-0 w-full m-2" asChild>
<Link href={"https://osu.ppy.sh/home/download"}>Download</Link>
<Link href={"https://osu.ppy.sh/home/download"}>
{t("howToStart.downloadTile.button")}
</Link>
</Button>
</div>
</PrettyHeader>
<PrettyHeader icon={<DoorOpen />} className="rounded-lg">
<div className="flex flex-col md:flex-row space-y-2 w-full">
<div className="w-full flex flex-col mx-2">
<p className="text-lg">Register osu!sunrise account</p>
<p className="text-lg">{t("howToStart.registerTile.title")}</p>
<p className="text-muted-foreground text-sm">
Account will allow you to join the osu!sunrise community
{t("howToStart.registerTile.description")}
</p>
</div>
<Button className="md:w-1/3 md:m-0 w-full m-2" asChild>
<Link href="/register">Sign up</Link>
<Link href="/register">
{t("howToStart.registerTile.button")}
</Link>
</Button>
</div>
</PrettyHeader>
<PrettyHeader icon={<BookOpenCheck />} className="rounded-lg">
<div className="flex flex-col md:flex-row space-y-2 w-full">
<div className="w-full flex flex-col mx-2">
<p className="text-lg">Follow the connection guide</p>
<p className="text-lg">{t("howToStart.guideTile.title")}</p>
<p className="text-muted-foreground text-sm">
Which helps you set up your osu! client to connect to
osu!sunrise
{t("howToStart.guideTile.description")}
</p>
</div>
<Button className="md:w-1/3 md:m-0 w-full m-2" asChild>
<Link href="/wiki#How%20to%20connect">Open guide</Link>
<Link href="/wiki#How%20to%20connect">
{t("howToStart.guideTile.button")}
</Link>
</Button>
</div>
</PrettyHeader>
Expand Down
16 changes: 16 additions & 0 deletions app/(website)/beatmaps/[id]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Metadata } from "next";
import Page from "./page";
import { getT } from "@/lib/i18n/utils";

export async function generateMetadata(): Promise<Metadata> {
const t = await getT("pages.beatmaps.detail.meta");
return {
title: t("title"),
openGraph: {
title: t("title"),
},
};
}

export default Page;

Loading