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:
leander committed 2025-09-16 03:31:11 +02:00
1 parent 1434528e4f
commit 017f5154b1
22 files changed
+204 -163

No files matched your search

+5
View File
@@ -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"
+5
View File
@@ -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
View File
@@ -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: {
@@ -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}
@@ -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>
)
}
+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 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({})
}
+5 -2
View File
@@ -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
View File
@@ -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>
)
}
+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[]) => {
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
View File
@@ -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
? [
+1
View File
@@ -9,6 +9,7 @@
background: none;
border: none;
color: var(--link-color);
cursor: pointer;
}
.button:hover,
.button *:hover {
+5 -2
View File
@@ -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;
}
+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*"],
}