mirror of
https://github.com/UiO-Gaming/uiogaming.no.git
synced 2026-11-05 13:56:47 +01:00
Rewrite translation logic
Rerendering translations apparently caused issues with safari because apple fucking sucks. Thus we had to rewrite the logic to use prefix-based translations. This is the attempt at this. It might be a mega commit but it's how it ought to be...
This commit is contained in:
22 files changed
+204
-163
No files matched your search
@@ -6,6 +6,11 @@
|
||||
"complaint": "Complaint",
|
||||
"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": {
|
||||
"back": "back",
|
||||
"flagImageAlt": "Norwegian flag"
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
"complaint": "Klage",
|
||||
"documents": "Dokumenter"
|
||||
},
|
||||
"metaTags": {
|
||||
"time": "Tid",
|
||||
"location": "Sted",
|
||||
"description": "En hobbyforening som ønsker skape et sosialt miljø rundt gaming og e-sport på UiO!"
|
||||
},
|
||||
"buttons": {
|
||||
"back": "tilbake",
|
||||
"flagImageAlt": "Britisk flagg"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import type { NextConfig } from "next"
|
||||
import createNextIntlPlugin from "next-intl/plugin"
|
||||
|
||||
const withNextIntl = createNextIntlPlugin()
|
||||
const withNextIntl = createNextIntlPlugin("./src/i18n.ts")
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
images: {
|
||||
|
||||
File renamed without changes.
@@ -1,4 +1,4 @@
|
||||
import Layout from "@/app/layout"
|
||||
import Layout from "@/app/[locale]/layout"
|
||||
|
||||
import Footer from "@/components/page-sections/footer"
|
||||
import { generateSeoMetadata, generateSeoViewport } from "@/components/seo"
|
||||
@@ -9,6 +9,7 @@ import { getBlogPost } from "@/lib/sanity"
|
||||
|
||||
import { PortableText, PortableTextComponents } from "@portabletext/react"
|
||||
import { Metadata, Viewport } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
import { getLocale } from "next-intl/server"
|
||||
import Image from "next/image"
|
||||
|
||||
@@ -17,6 +18,7 @@ import * as styles from "./page.module.css"
|
||||
interface BlogPageParams {
|
||||
params: Promise<{
|
||||
slug: string
|
||||
locale: string
|
||||
}>
|
||||
}
|
||||
|
||||
@@ -39,12 +41,6 @@ export async function generateMetadata({
|
||||
const { slug } = await params
|
||||
const data = await getBlogPost(slug)
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
title: "Blog Post Not Found | UiO Gaming",
|
||||
}
|
||||
}
|
||||
|
||||
return generateSeoMetadata({
|
||||
title: data.title,
|
||||
description: data.excerpt,
|
||||
@@ -103,9 +99,7 @@ const BlogPost = async ({ params }: BlogPageParams) => {
|
||||
|
||||
const data = await getBlogPost(slug)
|
||||
|
||||
if (!data) {
|
||||
throw new Error("Blog post not found")
|
||||
}
|
||||
if (!data) return notFound()
|
||||
|
||||
const createdAt = new Date(data._createdAt).toLocaleDateString(locale, {
|
||||
day: "numeric",
|
||||
@@ -136,7 +130,7 @@ const BlogPost = async ({ params }: BlogPageParams) => {
|
||||
{data.author?.image && (
|
||||
<Image
|
||||
className="circular"
|
||||
alt={`Forfatteren av innlegget, ${data.author.name}`}
|
||||
alt={data.author.name}
|
||||
src={urlFor(data.author.image).width(50).height(50).url()}
|
||||
width={50}
|
||||
height={50}
|
||||
File renamed without changes.
@@ -1,10 +1,13 @@
|
||||
import Layout from "@/app/[locale]/layout"
|
||||
|
||||
import { generateSeoMetadata, generateSeoViewport } from "@/components/seo"
|
||||
import Back from "@/components/ui/back"
|
||||
|
||||
import { getEvent } from "@/lib/sanity"
|
||||
|
||||
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 * as styles from "./page.module.css"
|
||||
@@ -12,6 +15,7 @@ import * as styles from "./page.module.css"
|
||||
interface EventPageParams {
|
||||
params: Promise<{
|
||||
slug: string
|
||||
locale: string
|
||||
}>
|
||||
}
|
||||
|
||||
@@ -21,11 +25,8 @@ export async function generateMetadata({
|
||||
const { slug } = await params
|
||||
const data = await getEvent(slug)
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
title: "Event Not Found | UiO Gaming",
|
||||
}
|
||||
}
|
||||
const locale = await getLocale()
|
||||
const t = await getTranslations("metaTags")
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
@@ -38,12 +39,12 @@ export async function generateMetadata({
|
||||
minute: "2-digit",
|
||||
timeZone: "Europe/Oslo",
|
||||
}
|
||||
return date.toLocaleString("no", options)
|
||||
return date.toLocaleString(locale, options)
|
||||
}
|
||||
|
||||
return generateSeoMetadata({
|
||||
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)
|
||||
|
||||
if (!data) {
|
||||
throw new Error("Event not found")
|
||||
}
|
||||
if (!data) return notFound()
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
@@ -77,21 +76,23 @@ const Event = async ({ params }: EventPageParams) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Back />
|
||||
<div className={styles.event}>
|
||||
<h1>{data.title}</h1>
|
||||
<div className={styles.metadata}>
|
||||
<FaMapMarkerAlt />
|
||||
<p>{data.location}</p>
|
||||
<Layout>
|
||||
<div className={styles.container}>
|
||||
<Back />
|
||||
<div className={styles.event}>
|
||||
<h1>{data.title}</h1>
|
||||
<div className={styles.metadata}>
|
||||
<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 className={styles.metadata}>
|
||||
<FaCalendarAlt />
|
||||
<p>{formatDate(data.date)}</p>
|
||||
</div>
|
||||
<p className={styles.description}>{data.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 BoardMembers from "@/components/page-sections/boardMembers"
|
||||
@@ -13,7 +13,7 @@ import { Metadata, Viewport } from "next"
|
||||
|
||||
export const revalidate = 3600 // 1 hour
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return generateSeoMetadata({})
|
||||
}
|
||||
|
||||
@@ -3,14 +3,17 @@ import Layout from "@/app/layout"
|
||||
import { generateSeoMetadata, generateSeoViewport } from "@/components/seo"
|
||||
|
||||
import { Metadata, Viewport } from "next"
|
||||
import { getTranslations } from "next-intl/server"
|
||||
import { useTranslations } from "next-intl"
|
||||
import Link from "next/link"
|
||||
|
||||
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({
|
||||
title: "404: Ikke funnet",
|
||||
title: t("title"),
|
||||
description: "Uh oh :(",
|
||||
})
|
||||
}
|
||||
|
||||
+5
-27
@@ -1,12 +1,6 @@
|
||||
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 { getLocale } from "next-intl/server"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "UiO Gaming",
|
||||
@@ -19,30 +13,14 @@ export const metadata: Metadata = {
|
||||
},
|
||||
}
|
||||
|
||||
export default async function RootLayout({
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
}: {
|
||||
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 (
|
||||
<html lang={locale}>
|
||||
<body>
|
||||
<LocaleProvider locale={locale} messages={messages}>
|
||||
<main>{children}</main>
|
||||
{analyticsConfig?.enabled && (
|
||||
<UmamiAnalytics
|
||||
websiteId={analyticsConfig.websiteId}
|
||||
src={analyticsConfig.src}
|
||||
domains={analyticsConfig.domains}
|
||||
/>
|
||||
)}
|
||||
</LocaleProvider>
|
||||
</body>
|
||||
<html>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
export default function RootPage() {
|
||||
redirect("/en/404")
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -50,7 +50,7 @@ const Events = async () => {
|
||||
const mapEvents = (events: Event[]) => {
|
||||
if (events.length !== 0) {
|
||||
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}>
|
||||
<h3>{event.title}</h3>
|
||||
<div>
|
||||
|
||||
+13
-6
@@ -1,4 +1,5 @@
|
||||
import { Metadata, Viewport } from "next"
|
||||
import { getLocale, getTranslations } from "next-intl/server"
|
||||
|
||||
type SeoProps = {
|
||||
title?: string
|
||||
@@ -7,16 +8,17 @@ type SeoProps = {
|
||||
author?: string
|
||||
}
|
||||
|
||||
export function generateSeoMetadata({
|
||||
export async function generateSeoMetadata({
|
||||
title,
|
||||
description,
|
||||
imageURL,
|
||||
author,
|
||||
}: SeoProps): Metadata {
|
||||
}: SeoProps): Promise<Metadata> {
|
||||
const t = await getTranslations("metaTags")
|
||||
const locale = await getLocale()
|
||||
|
||||
const defaultTitle = "UiO Gaming"
|
||||
const metaDescription =
|
||||
description ||
|
||||
"En hobbyforening som ønsker skape et sosialt miljö rundt gaming og e-sport på UiO!"
|
||||
const metaDescription = description || t("description")
|
||||
const siteUrl = "https://uiogaming.no"
|
||||
const themeColor = "#7d36e7"
|
||||
|
||||
@@ -24,11 +26,16 @@ export function generateSeoMetadata({
|
||||
title: title ? `${title} | ${defaultTitle}` : defaultTitle,
|
||||
description: metaDescription,
|
||||
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: {
|
||||
title: title || defaultTitle,
|
||||
description: metaDescription,
|
||||
type: "website",
|
||||
locale: "no",
|
||||
locale: locale,
|
||||
siteName: defaultTitle,
|
||||
images: imageURL
|
||||
? [
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--link-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
.button:hover,
|
||||
.button *:hover {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"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 * as styles from "./back.module.css"
|
||||
|
||||
const Back = () => {
|
||||
const locale = useLocale()
|
||||
const router = useRouter()
|
||||
const t = useTranslations("buttons")
|
||||
|
||||
const handleBack = () => {
|
||||
@@ -18,7 +21,7 @@ const Back = () => {
|
||||
if (window.history.length > 1 && isSameDomain) {
|
||||
window.history.back()
|
||||
} 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;
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
})
|
||||
@@ -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*"],
|
||||
}
|
||||
Reference in new issue
Block a user