Merge pull request #51 from UiO-Gaming/fix/translation-rewrite

fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple
This commit is contained in:
leander authored and GitHub committed 2025-09-16 04:09:47 +02:00
commit 2591eacd14
22 files changed
+204 -163

No files matched your search

+5
View File
@@ -6,6 +6,11 @@
"complaint": "Complaint", "complaint": "Complaint",
"documents": "Documents" "documents": "Documents"
}, },
"metaTags": {
"time": "Time",
"location": "Location",
"description": "A student organisation that aims to create an inclusive environment around gaming and e-sports for the students at UiO"
},
"buttons": { "buttons": {
"back": "back", "back": "back",
"flagImageAlt": "Norwegian flag" "flagImageAlt": "Norwegian flag"
+5
View File
@@ -6,6 +6,11 @@
"complaint": "Klage", "complaint": "Klage",
"documents": "Dokumenter" "documents": "Dokumenter"
}, },
"metaTags": {
"time": "Tid",
"location": "Sted",
"description": "En hobbyforening som ønsker skape et sosialt miljø rundt gaming og e-sport på UiO!"
},
"buttons": { "buttons": {
"back": "tilbake", "back": "tilbake",
"flagImageAlt": "Britisk flagg" "flagImageAlt": "Britisk flagg"
+1 -1
View File
@@ -1,7 +1,7 @@
import type { NextConfig } from "next" import type { NextConfig } from "next"
import createNextIntlPlugin from "next-intl/plugin" import createNextIntlPlugin from "next-intl/plugin"
const withNextIntl = createNextIntlPlugin() const withNextIntl = createNextIntlPlugin("./src/i18n.ts")
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
images: { images: {
@@ -1,4 +1,4 @@
import Layout from "@/app/layout" import Layout from "@/app/[locale]/layout"
import Footer from "@/components/page-sections/footer" import Footer from "@/components/page-sections/footer"
import { generateSeoMetadata, generateSeoViewport } from "@/components/seo" import { generateSeoMetadata, generateSeoViewport } from "@/components/seo"
@@ -9,6 +9,7 @@ import { getBlogPost } from "@/lib/sanity"
import { PortableText, PortableTextComponents } from "@portabletext/react" import { PortableText, PortableTextComponents } from "@portabletext/react"
import { Metadata, Viewport } from "next" import { Metadata, Viewport } from "next"
import { notFound } from "next/navigation"
import { getLocale } from "next-intl/server" import { getLocale } from "next-intl/server"
import Image from "next/image" import Image from "next/image"
@@ -17,6 +18,7 @@ import * as styles from "./page.module.css"
interface BlogPageParams { interface BlogPageParams {
params: Promise<{ params: Promise<{
slug: string slug: string
locale: string
}> }>
} }
@@ -39,12 +41,6 @@ export async function generateMetadata({
const { slug } = await params const { slug } = await params
const data = await getBlogPost(slug) const data = await getBlogPost(slug)
if (!data) {
return {
title: "Blog Post Not Found | UiO Gaming",
}
}
return generateSeoMetadata({ return generateSeoMetadata({
title: data.title, title: data.title,
description: data.excerpt, description: data.excerpt,
@@ -103,9 +99,7 @@ const BlogPost = async ({ params }: BlogPageParams) => {
const data = await getBlogPost(slug) const data = await getBlogPost(slug)
if (!data) { if (!data) return notFound()
throw new Error("Blog post not found")
}
const createdAt = new Date(data._createdAt).toLocaleDateString(locale, { const createdAt = new Date(data._createdAt).toLocaleDateString(locale, {
day: "numeric", day: "numeric",
@@ -136,7 +130,7 @@ const BlogPost = async ({ params }: BlogPageParams) => {
{data.author?.image && ( {data.author?.image && (
<Image <Image
className="circular" className="circular"
alt={`Forfatteren av innlegget, ${data.author.name}`} alt={data.author.name}
src={urlFor(data.author.image).width(50).height(50).url()} src={urlFor(data.author.image).width(50).height(50).url()}
width={50} width={50}
height={50} height={50}
@@ -1,10 +1,13 @@
import Layout from "@/app/[locale]/layout"
import { generateSeoMetadata, generateSeoViewport } from "@/components/seo" import { generateSeoMetadata, generateSeoViewport } from "@/components/seo"
import Back from "@/components/ui/back" import Back from "@/components/ui/back"
import { getEvent } from "@/lib/sanity" import { getEvent } from "@/lib/sanity"
import { Metadata, Viewport } from "next" import { Metadata, Viewport } from "next"
import { getLocale } from "next-intl/server" import { notFound } from "next/navigation"
import { getLocale, getTranslations } from "next-intl/server"
import { FaCalendarAlt, FaMapMarkerAlt } from "react-icons/fa" import { FaCalendarAlt, FaMapMarkerAlt } from "react-icons/fa"
import * as styles from "./page.module.css" import * as styles from "./page.module.css"
@@ -12,6 +15,7 @@ import * as styles from "./page.module.css"
interface EventPageParams { interface EventPageParams {
params: Promise<{ params: Promise<{
slug: string slug: string
locale: string
}> }>
} }
@@ -21,11 +25,8 @@ export async function generateMetadata({
const { slug } = await params const { slug } = await params
const data = await getEvent(slug) const data = await getEvent(slug)
if (!data) { const locale = await getLocale()
return { const t = await getTranslations("metaTags")
title: "Event Not Found | UiO Gaming",
}
}
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
const date = new Date(dateString) const date = new Date(dateString)
@@ -38,12 +39,12 @@ export async function generateMetadata({
minute: "2-digit", minute: "2-digit",
timeZone: "Europe/Oslo", timeZone: "Europe/Oslo",
} }
return date.toLocaleString("no", options) return date.toLocaleString(locale, options)
} }
return generateSeoMetadata({ return generateSeoMetadata({
title: data.title, title: data.title,
description: `Tid: ${formatDate(data.date)}\nSted: ${data.location}\n\n${data.description}`, description: `${t("time")}: ${formatDate(data.date)}\n${t("location")}: ${data.location}\n\n${data.description}`,
}) })
} }
@@ -57,9 +58,7 @@ const Event = async ({ params }: EventPageParams) => {
const data = await getEvent(slug) const data = await getEvent(slug)
if (!data) { if (!data) return notFound()
throw new Error("Event not found")
}
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
const date = new Date(dateString) const date = new Date(dateString)
@@ -77,21 +76,23 @@ const Event = async ({ params }: EventPageParams) => {
} }
return ( return (
<div className={styles.container}> <Layout>
<Back /> <div className={styles.container}>
<div className={styles.event}> <Back />
<h1>{data.title}</h1> <div className={styles.event}>
<div className={styles.metadata}> <h1>{data.title}</h1>
<FaMapMarkerAlt /> <div className={styles.metadata}>
<p>{data.location}</p> <FaMapMarkerAlt />
<p>{data.location}</p>
</div>
<div className={styles.metadata}>
<FaCalendarAlt />
<p>{formatDate(data.date)}</p>
</div>
<p className={styles.description}>{data.description}</p>
</div> </div>
<div className={styles.metadata}>
<FaCalendarAlt />
<p>{formatDate(data.date)}</p>
</div>
<p className={styles.description}>{data.description}</p>
</div> </div>
</div> </Layout>
) )
} }
+32
View File
@@ -0,0 +1,32 @@
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from "next-intl"
import { getLocale } from "next-intl/server"
import LanguageSelector from "@components/ui/languageSelector"
import UmamiAnalytics from "@/components/umamiAnalytics"
import { getAnalyticsConfig } from "@/lib/analyticsConfig"
export default async function LocaleLayout({
children,
}: {
children: React.ReactNode
}) {
const locale = (await getLocale()) as "no" | "en"
const messages = (await import(`../../../messages/${locale}.json`)).default
const analyticsConfig = getAnalyticsConfig()
return (
<NextIntlClientProvider locale={locale} messages={messages}>
<LanguageSelector />
<main>{children}</main>
{analyticsConfig?.enabled && (
<UmamiAnalytics
websiteId={analyticsConfig.websiteId}
src={analyticsConfig.src}
domains={analyticsConfig.domains}
/>
)}
</NextIntlClientProvider>
)
}
@@ -1,4 +1,4 @@
import Layout from "@/app/layout" import Layout from "@/app/[locale]/layout"
import About from "@/components/page-sections/about" import About from "@/components/page-sections/about"
import BoardMembers from "@/components/page-sections/boardMembers" import BoardMembers from "@/components/page-sections/boardMembers"
@@ -13,7 +13,7 @@ import { Metadata, Viewport } from "next"
export const revalidate = 3600 // 1 hour export const revalidate = 3600 // 1 hour
export function generateMetadata(): Metadata { export async function generateMetadata(): Promise<Metadata> {
return generateSeoMetadata({}) return generateSeoMetadata({})
} }
+5 -2
View File
@@ -3,14 +3,17 @@ import Layout from "@/app/layout"
import { generateSeoMetadata, generateSeoViewport } from "@/components/seo" import { generateSeoMetadata, generateSeoViewport } from "@/components/seo"
import { Metadata, Viewport } from "next" import { Metadata, Viewport } from "next"
import { getTranslations } from "next-intl/server"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import Link from "next/link" import Link from "next/link"
import * as styles from "./global-not-found.module.css" import * as styles from "./global-not-found.module.css"
export function generateMetadata(): Metadata { export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations("404")
return generateSeoMetadata({ return generateSeoMetadata({
title: "404: Ikke funnet", title: t("title"),
description: "Uh oh :(", description: "Uh oh :(",
}) })
} }
+5 -27
View File
@@ -1,12 +1,6 @@
import "@/app/globals.css" import "@/app/globals.css"
import { LocaleProvider } from "@/components/localeProvider"
import UmamiAnalytics from "@/components/umamiAnalytics"
import { getAnalyticsConfig } from "@/lib/analyticsConfig"
import type { Metadata } from "next" import type { Metadata } from "next"
import { getLocale } from "next-intl/server"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "UiO Gaming", title: "UiO Gaming",
@@ -19,30 +13,14 @@ export const metadata: Metadata = {
}, },
} }
export default async function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: {
children: React.ReactNode children: React.ReactNode
}>) { }) {
const locale = (await getLocale()) as "no" | "en"
const messages = (await import(`../../messages/${locale}.json`)).default // TODO: duplicate of request.ts?
const analyticsConfig = getAnalyticsConfig()
return ( return (
<html lang={locale}> <html>
<body> <body>{children}</body>
<LocaleProvider locale={locale} messages={messages}>
<main>{children}</main>
{analyticsConfig?.enabled && (
<UmamiAnalytics
websiteId={analyticsConfig.websiteId}
src={analyticsConfig.src}
domains={analyticsConfig.domains}
/>
)}
</LocaleProvider>
</body>
</html> </html>
) )
} }
+5
View File
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation"
export default function RootPage() {
redirect("/en/404")
}
-24
View File
@@ -1,24 +0,0 @@
.button {
display: flex;
gap: 0.25rem;
position: fixed;
top: 0.5rem;
right: 0.5rem;
z-index: 0;
background: pink;
color: black;
border: 1px solid black;
font-size: 1.1rem;
border-radius: 0.5rem;
padding-inline: 0.5em;
}
.button:hover {
background: #ff7b7b;
color: white;
}
.flag {
width: 1.2rem;
}
-63
View File
@@ -1,63 +0,0 @@
"use client"
import { GB, NO } from "country-flag-icons/react/3x2"
import { NextIntlClientProvider } from "next-intl"
import { ReactNode, useEffect, useState } from "react"
import * as styles from "./localeProvider.module.css"
type LocaleProviderProps = {
locale: "no" | "en"
messages: Record<string, string>
children: ReactNode
}
export function LocaleProvider({
locale: serverLocale,
messages,
children,
}: LocaleProviderProps) {
const [locale, setLocale] = useState<"no" | "en">(serverLocale)
const [loadedMessages, setLoadedMessages] =
useState<Record<string, string>>(messages)
useEffect(() => {
const match = document.cookie.match(/(?:^|; )locale=([^;]+)/)
const saved = match ? decodeURIComponent(match[1]) : null
if (saved && saved !== locale) {
setLocale(saved as "no" | "en")
import(`../../messages/${saved}.json`).then(module => {
setLoadedMessages(module.default)
})
}
}, [locale])
const changeLocale = (newLocale: "no" | "en") => {
setLocale(newLocale)
document.cookie = `locale=${encodeURIComponent(
newLocale
)}; path=/; max-age=31536000; SameSite=Lax`
window.location.reload()
}
return (
<NextIntlClientProvider locale={locale} messages={loadedMessages}>
{children}
<div className={styles.languageSwitcher}>
{locale === "no" ? (
<button onClick={() => changeLocale("en")} className={styles.button}>
<GB className={styles.flag} title="English flag" />
English
</button>
) : (
<button onClick={() => changeLocale("no")} className={styles.button}>
<NO className={styles.flag} title="Norwegian flag" />
Norsk
</button>
)}
</div>
</NextIntlClientProvider>
)
}
+1 -1
View File
@@ -50,7 +50,7 @@ const Events = async () => {
const mapEvents = (events: Event[]) => { const mapEvents = (events: Event[]) => {
if (events.length !== 0) { if (events.length !== 0) {
return events.map((event: Event) => ( return events.map((event: Event) => (
<Link key={event._id} href={"/event/" + event.slug.current}> <Link key={event._id} href={`/${locale}/event/${event.slug.current}`}>
<article className={styles.card}> <article className={styles.card}>
<h3>{event.title}</h3> <h3>{event.title}</h3>
<div> <div>
+13 -6
View File
@@ -1,4 +1,5 @@
import { Metadata, Viewport } from "next" import { Metadata, Viewport } from "next"
import { getLocale, getTranslations } from "next-intl/server"
type SeoProps = { type SeoProps = {
title?: string title?: string
@@ -7,16 +8,17 @@ type SeoProps = {
author?: string author?: string
} }
export function generateSeoMetadata({ export async function generateSeoMetadata({
title, title,
description, description,
imageURL, imageURL,
author, author,
}: SeoProps): Metadata { }: SeoProps): Promise<Metadata> {
const t = await getTranslations("metaTags")
const locale = await getLocale()
const defaultTitle = "UiO Gaming" const defaultTitle = "UiO Gaming"
const metaDescription = const metaDescription = description || t("description")
description ||
"En hobbyforening som ønsker skape et sosialt miljö rundt gaming og e-sport på UiO!"
const siteUrl = "https://uiogaming.no" const siteUrl = "https://uiogaming.no"
const themeColor = "#7d36e7" const themeColor = "#7d36e7"
@@ -24,11 +26,16 @@ export function generateSeoMetadata({
title: title ? `${title} | ${defaultTitle}` : defaultTitle, title: title ? `${title} | ${defaultTitle}` : defaultTitle,
description: metaDescription, description: metaDescription,
authors: author ? [{ name: author }] : [{ name: "UiO Gaming" }], authors: author ? [{ name: author }] : [{ name: "UiO Gaming" }],
icons: {
icon: "/logo_2024_neon_pink.png",
shortcut: "/logo_2024_neon_pink.png",
apple: "/logo_2024_neon_pink.png",
},
openGraph: { openGraph: {
title: title || defaultTitle, title: title || defaultTitle,
description: metaDescription, description: metaDescription,
type: "website", type: "website",
locale: "no", locale: locale,
siteName: defaultTitle, siteName: defaultTitle,
images: imageURL images: imageURL
? [ ? [
+1
View File
@@ -9,6 +9,7 @@
background: none; background: none;
border: none; border: none;
color: var(--link-color); color: var(--link-color);
cursor: pointer;
} }
.button:hover, .button:hover,
.button *:hover { .button *:hover {
+5 -2
View File
@@ -1,11 +1,14 @@
"use client" "use client"
import { useTranslations } from "next-intl" import { useLocale, useTranslations } from "next-intl"
import { useRouter } from "next/navigation"
import { FaArrowLeft } from "react-icons/fa" import { FaArrowLeft } from "react-icons/fa"
import * as styles from "./back.module.css" import * as styles from "./back.module.css"
const Back = () => { const Back = () => {
const locale = useLocale()
const router = useRouter()
const t = useTranslations("buttons") const t = useTranslations("buttons")
const handleBack = () => { const handleBack = () => {
@@ -18,7 +21,7 @@ const Back = () => {
if (window.history.length > 1 && isSameDomain) { if (window.history.length > 1 && isSameDomain) {
window.history.back() window.history.back()
} else { } else {
window.location.href = "/" router.push(`/${locale}`)
} }
} }
@@ -0,0 +1,30 @@
.languageSwitcher {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.button {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: rgba(0, 0, 0, 0.35);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
}
.button:hover {
background: rgba(0, 0, 0, 1);
}
.flag {
width: 20px;
height: 15px;
border-radius: 2px;
}
+37
View File
@@ -0,0 +1,37 @@
"use client"
import { GB, NO } from "country-flag-icons/react/3x2"
import { useLocale } from "next-intl"
import { useRouter, usePathname } from "next/navigation"
import * as styles from "./languageSelector.module.css"
const LanguageSelector = () => {
const locale = useLocale()
const router = useRouter()
const pathname = usePathname()
const changeLocale = (newLocale: "no" | "en") => {
// Remove the current locale from the pathname
const pathWithoutLocale = pathname.replace(/^\/[a-z]{2}/, "")
router.push(`/${newLocale}${pathWithoutLocale}`)
}
return (
<div className={styles.languageSwitcher}>
{locale === "no" ? (
<button onClick={() => changeLocale("en")} className={styles.button}>
<GB className={styles.flag} title="English flag" />
English
</button>
) : (
<button onClick={() => changeLocale("no")} className={styles.button}>
<NO className={styles.flag} title="Norwegian flag" />
Norsk
</button>
)}
</div>
)
}
export default LanguageSelector
+15
View File
@@ -0,0 +1,15 @@
import { notFound } from "next/navigation"
import { getRequestConfig } from "next-intl/server"
const locales = ["no", "en"]
export default getRequestConfig(async ({ requestLocale }) => {
const locale = await requestLocale
if (!locale || !locales.includes(locale as "no" | "en")) notFound()
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
}
})
+12
View File
@@ -0,0 +1,12 @@
import createMiddleware from "next-intl/middleware"
export default createMiddleware({
locales: ["no", "en"],
defaultLocale: "no",
localePrefix: "always",
})
export const config = {
// Match only internationalized pathnames
matcher: ["/", "/(no|en)/:path*"],
}