reintroduce translation support

This commit is contained in:
leander committed 2025-08-06 01:16:40 +02:00
1 parent b4bb073d87
commit 50bdf6503e
22 files changed
+482 -71

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
{
"common": {
"photo": "Photo",
"join": "Become a member",
"statutes": "Statutes",
"complaint": "Complaint",
"documents": "Documents"
},
"buttons": {
"back": "back",
"flagImageAlt": "Norwegian flag"
},
"404": {
"title": "404: Page not found",
"text": "Our journey ended here. Please don't leave us :(",
"back": "Back to safe grounds"
},
"hero": {
"text": "A student organisation that aims to create an inclusive environment around gaming and e-sports for the students at UiO"
},
"about": {
"title": "Who are we?",
"imageAlt": "2 members playing together",
"text": "UiO Gaming is a student organisation with a primary focus on creating an inclusive environment around gaming and e-sports. We wish to establish a community where it’s easy to make new friends and cultivate your gaming interests. Each week we host both casual and more intense gamenights, as well as physical events such as board game nights and other social gatherings. Our main channel is a Discord server where you can meet new people with common interests and play games with other students.",
"interested": "Interested?",
"other": "Other"
},
"events": {
"title": "Upcoming Events",
"imageAlt": "Members playing board games",
"readMore": "Read more",
"noEvents": "No upcoming events"
},
"office": {
"title": "The Office",
"text": "We have an office at Chateau Neuf where you can use our gear and hang out with other members! We've got all the latest and greatest consoles, 6 high-end PCs as well as a plethora of board games.",
"tip": "Tip: You can look around the office by clicking and dragging the image below 👀"
},
"boardMembers": {
"title": "The Board",
"imageAlt": "Potrait of board member",
"noContact": "No contact info"
},
"contact": {
"title": "Find us",
"email": "E-mail"
},
"footer": {
"sourceCode": "Source code",
"admin": "Admin page",
"orgNum": "Org. No."
}
}
+53
View File
@@ -0,0 +1,53 @@
{
"common": {
"photo": "Foto",
"join": "Bli medlem",
"statutes": "Vedtekter",
"complaint": "Klage",
"documents": "Dokumenter"
},
"buttons": {
"back": "tilbake",
"flagImageAlt": "Britisk flagg"
},
"404": {
"title": "404: Siden finnes ikke",
"text": "Du har nådd en blindvei, men ikke på Blindern",
"back": "Tilbake til hjemmesiden"
},
"hero": {
"text": "En hobbyforening som ønsker å skape et sosialt miljø rundt gaming og e-sport på UiO"
},
"about": {
"title": "Hvem er vi?",
"imageAlt": "2 medlemmer som spiller sammen",
"text": "UiO Gaming er en forening med hovedfokus på å danne et inkluderende fellesskap rundt videospill og e-sport. Hos oss er det enkelt å skaffe seg venner, ettersom alle har felles interesser. Vi arrangerer ukentlige spillkvelder og fysiske sosiale arrangementer, som for eksempel brettspillkvelder og sosiale sammenkomster. Vår hovedkanal er en aktiv Discord-server hvor man kan møte nye folk med felles interesser og game med andre studenter.",
"interested": "Interessert?",
"other": "Annet"
},
"events": {
"title": "Hva skjer?",
"imageAlt": "Medlemmer som spiller brettspill",
"readMore": "Les mer",
"noEvents": "Ingen kommende arrangementer"
},
"office": {
"title": "Kontoret",
"text": "Vi har et foreningskontor på Chateau Neuf der man kan bruke utstyret vårt og henge med andre medlemmer! Vi har blant annet alle de nyeste konsollene, 6 gaming-PCer samt et bredt utvalg av brettspill.",
"tip": "Tips: Du kan kikke rundt i kontoret ved å klikke og dra på bildet nedenfor 👀"
},
"boardMembers": {
"title": "Styret",
"imageAlt": "Bilde av styremedlem",
"noContact": "Ingen kontaktinfo"
},
"contact": {
"title": "Finn oss",
"email": "E-post"
},
"footer": {
"sourceCode": "Kildekode",
"admin": "Adminområdet",
"orgNum": "Org.Nr."
}
}
+4 -1
View File
@@ -1,4 +1,7 @@
import type { NextConfig } from "next" import type { NextConfig } from "next"
import createNextIntlPlugin from "next-intl/plugin"
const withNextIntl = createNextIntlPlugin()
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
images: { images: {
@@ -13,4 +16,4 @@ const nextConfig: NextConfig = {
}, },
} }
export default nextConfig export default withNextIntl(nextConfig)
+143 -1
View File
@@ -11,8 +11,10 @@
"@portabletext/react": "^3.2.1", "@portabletext/react": "^3.2.1",
"@sanity/client": "^7.8.1", "@sanity/client": "^7.8.1",
"@sanity/image-url": "^1.1.0", "@sanity/image-url": "^1.1.0",
"country-flag-icons": "^1.5.19",
"install": "^0.13.0", "install": "^0.13.0",
"next": "15.4.4", "next": "15.4.4",
"next-intl": "^4.3.4",
"npm": "^11.5.2", "npm": "^11.5.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
@@ -204,6 +206,66 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@formatjs/ecma402-abstract": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz",
"integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==",
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/intl-localematcher": "0.6.1",
"decimal.js": "^10.4.3",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz",
"integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/fast-memoize": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/icu-messageformat-parser": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz",
"integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"@formatjs/icu-skeleton-parser": "1.8.14",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/icu-skeleton-parser": {
"version": "1.8.14",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz",
"integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz",
"integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==",
"license": "MIT",
"dependencies": {
"tslib": "2"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -990,6 +1052,12 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/@schummar/icu-type-parser": {
"version": "1.21.5",
"resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz",
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
"license": "MIT"
},
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.15", "version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -2110,6 +2178,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/country-flag-icons": {
"version": "1.5.19",
"resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.5.19.tgz",
"integrity": "sha512-D/ZkRyj+ywJC6b2IrAN3/tpbReMUqmuRLlcKFoY/o0+EPQN9Ev/e8tV+D3+9scvu/tarxwLErNwS73C3yzxs/g==",
"license": "MIT"
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2211,6 +2285,12 @@
} }
} }
}, },
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
"node_modules/decompress-response": { "node_modules/decompress-response": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-7.0.0.tgz", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-7.0.0.tgz",
@@ -3464,6 +3544,18 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/intl-messageformat": {
"version": "10.7.16",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz",
"integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==",
"license": "BSD-3-Clause",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/icu-messageformat-parser": "2.11.2",
"tslib": "^2.8.0"
}
},
"node_modules/is-array-buffer": { "node_modules/is-array-buffer": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -4181,6 +4273,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/next": { "node_modules/next": {
"version": "15.4.4", "version": "15.4.4",
"resolved": "https://registry.npmjs.org/next/-/next-15.4.4.tgz", "resolved": "https://registry.npmjs.org/next/-/next-15.4.4.tgz",
@@ -4233,6 +4334,33 @@
} }
} }
}, },
"node_modules/next-intl": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.4.tgz",
"integrity": "sha512-VWLIDlGbnL/o4LnveJTJD1NOYN8lh3ZAGTWw2krhfgg53as3VsS4jzUVnArJdqvwtlpU/2BIDbWTZ7V4o1jFEw==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/amannn"
}
],
"license": "MIT",
"dependencies": {
"@formatjs/intl-localematcher": "^0.5.4",
"negotiator": "^1.0.0",
"use-intl": "^4.3.4"
},
"peerDependencies": {
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0",
"typescript": "^5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/npm": { "node_modules/npm": {
"version": "11.5.2", "version": "11.5.2",
"resolved": "https://registry.npmjs.org/npm/-/npm-11.5.2.tgz", "resolved": "https://registry.npmjs.org/npm/-/npm-11.5.2.tgz",
@@ -7982,7 +8110,7 @@
"version": "5.8.3", "version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@@ -8062,6 +8190,20 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-intl": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.3.4.tgz",
"integrity": "sha512-sHfiU0QeJ1rirNWRxvCyvlSh9+NczcOzRnPyMeo2rtHXhVnBsvMRjE+UG4eh3lRhCxrvcqei/I0lBxsc59on1w==",
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "^2.2.0",
"@schummar/icu-type-parser": "1.21.5",
"intl-messageformat": "^10.5.14"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+2
View File
@@ -15,8 +15,10 @@
"@portabletext/react": "^3.2.1", "@portabletext/react": "^3.2.1",
"@sanity/client": "^7.8.1", "@sanity/client": "^7.8.1",
"@sanity/image-url": "^1.1.0", "@sanity/image-url": "^1.1.0",
"country-flag-icons": "^1.5.19",
"install": "^0.13.0", "install": "^0.13.0",
"next": "15.4.4", "next": "15.4.4",
"next-intl": "^4.3.4",
"npm": "^11.5.2", "npm": "^11.5.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
+3 -1
View File
@@ -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 { getLocale } from "next-intl/server"
import Image from "next/image" import Image from "next/image"
import * as styles from "./page.module.css" import * as styles from "./page.module.css"
@@ -98,6 +99,7 @@ const components: PortableTextComponents = {
const BlogPost = async ({ params }: BlogPageParams) => { const BlogPost = async ({ params }: BlogPageParams) => {
const { slug }: { slug: string } = await params const { slug }: { slug: string } = await params
const locale = await getLocale()
const data = await getBlogPost(slug) const data = await getBlogPost(slug)
@@ -105,7 +107,7 @@ const BlogPost = async ({ params }: BlogPageParams) => {
throw new Error("Blog post not found") throw new Error("Blog post not found")
} }
const createdAt = new Date(data._createdAt).toLocaleDateString("nb-NO", { const createdAt = new Date(data._createdAt).toLocaleDateString(locale, {
day: "numeric", day: "numeric",
month: "long", month: "long",
year: "numeric", year: "numeric",
+3 -1
View File
@@ -4,6 +4,7 @@ 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 { 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"
@@ -52,6 +53,7 @@ export function generateViewport(): Viewport {
const Event = async ({ params }: EventPageParams) => { const Event = async ({ params }: EventPageParams) => {
const { slug }: { slug: string } = await params const { slug }: { slug: string } = await params
const locale = await getLocale()
const data = await getEvent(slug) const data = await getEvent(slug)
@@ -71,7 +73,7 @@ const Event = async ({ params }: EventPageParams) => {
timeZone: "Europe/Oslo", timeZone: "Europe/Oslo",
} }
return date.toLocaleString("no", options) return date.toLocaleString(locale, options)
} }
return ( return (
+6 -3
View File
@@ -3,6 +3,7 @@ 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 { 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"
@@ -19,18 +20,20 @@ export function generateViewport(): Viewport {
} }
const NotFoundPage = () => { const NotFoundPage = () => {
const t = useTranslations("404")
return ( return (
<Layout> <Layout>
<div className={styles.container}> <div className={styles.container}>
<h1 className={styles.centeredText}>404: Siden ble ikke funnet</h1> <h1 className={styles.centeredText}>{t("title")}</h1>
<p className={styles.centeredText}> <p className={styles.centeredText}>
Reisen vår endte her. Vennligst ikke forlat oss {t("text")}{" "}
<span role="img" aria-label="eyes"> <span role="img" aria-label="eyes">
👀 👀
</span> </span>
</p> </p>
<Link href="/" className={`${styles.centeredText} ${styles.homeLink}`}> <Link href="/" className={`${styles.centeredText} ${styles.homeLink}`}>
Tilbake til trygg grunn {t("back")}
</Link> </Link>
</div> </div>
</Layout> </Layout>
+17 -10
View File
@@ -1,10 +1,12 @@
import "@/app/globals.css" import "@/app/globals.css"
import { LocaleProvider } from "@/components/localeProvider"
import UmamiAnalytics from "@/components/umamiAnalytics" import UmamiAnalytics from "@/components/umamiAnalytics"
import { getAnalyticsConfig } from "@/lib/analyticsConfig" 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",
@@ -17,24 +19,29 @@ export const metadata: Metadata = {
}, },
} }
export default function RootLayout({ export default async function RootLayout({
children, children,
}: Readonly<{ }: 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() const analyticsConfig = getAnalyticsConfig()
return ( return (
<html lang="no"> <html lang={locale}>
<body> <body>
<main>{children}</main> <LocaleProvider locale={locale} messages={messages}>
{analyticsConfig?.enabled && ( <main>{children}</main>
<UmamiAnalytics {analyticsConfig?.enabled && (
websiteId={analyticsConfig.websiteId} <UmamiAnalytics
src={analyticsConfig.src} websiteId={analyticsConfig.websiteId}
domains={analyticsConfig.domains} src={analyticsConfig.src}
/> domains={analyticsConfig.domains}
)} />
)}
</LocaleProvider>
</body> </body>
</html> </html>
) )
+24
View File
@@ -0,0 +1,24 @@
.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
@@ -0,0 +1,63 @@
"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>
)
}
+11 -14
View File
@@ -2,38 +2,35 @@ import DiscordButton from "@components/ui/discordButton"
import MemberButton from "@components/ui/memberButton" import MemberButton from "@components/ui/memberButton"
import OtherButton from "@components/ui/otherButton" import OtherButton from "@components/ui/otherButton"
import { useTranslations } from "next-intl"
import Image from "next/image" import Image from "next/image"
import * as styles from "./about.module.css" import * as styles from "./about.module.css"
const About = () => { const About = () => {
const t = useTranslations("about")
const tCommon = useTranslations("common")
return ( return (
<section> <section>
<div className={styles.container}> <div className={styles.container}>
<div className={"no-mobile " + styles.image}> <div className={"no-mobile " + styles.image}>
<Image <Image
src="/about.jpg" src="/about.jpg"
alt="To medlemmer spiller sammen" alt={t("imageAlt")}
width={640} width={640}
height={480} height={480}
/> />
<p className="photo-credit">Foto: 장태민, Studentenes Fotoklubb</p> <p className="photo-credit">
{tCommon("photo")}: 장태민, Studentenes Fotoklubb
</p>
</div> </div>
<div> <div>
<h2 className="left-section-header">Hvem er vi?</h2> <h2 className="left-section-header">{t("title")}</h2>
<div> <div>
<p> <p>{t("text")}</p>
UiO Gaming er en forening med hovedfokus på å danne et
inkluderende fellesskap rundt videospill og e-sport. Hos oss er
det enkelt å skaffe seg venner, ettersom alle har felles
interesser. Vi arrangerer ukentlige spillkvelder og fysiske
sosiale arrangementer, som for eksempel brettspillkvelder og
sosiale sammenkomster. Vår hovedkanal er en aktiv Discord-server
hvor man kan møte nye folk med felles interesser og game med andre
studenter.
</p>
<div className={styles.joinSection}> <div className={styles.joinSection}>
<h3>Interessert?</h3> <h3>{t("interested")}</h3>
<div className={styles.buttons}> <div className={styles.buttons}>
<MemberButton /> <MemberButton />
<DiscordButton /> <DiscordButton />
@@ -1,6 +1,7 @@
import { urlFor } from "@/lib/imageUrlBuilder" import { urlFor } from "@/lib/imageUrlBuilder"
import { sanityClient } from "@/lib/sanity" import { sanityClient } from "@/lib/sanity"
import { getLocale, getTranslations } from "next-intl/server"
import Image from "next/image" import Image from "next/image"
import * as styles from "./boardMembers.module.css" import * as styles from "./boardMembers.module.css"
@@ -27,6 +28,9 @@ interface BoardMember {
} }
const BoardMembers = async () => { const BoardMembers = async () => {
const t = await getTranslations("boardMembers")
const locale = (await getLocale()) as "no" | "en"
const query = ` const query = `
*[_type == "author" && boardMember == true] | order(orderRank asc) { *[_type == "author" && boardMember == true] | order(orderRank asc) {
_id, _id,
@@ -53,7 +57,7 @@ const BoardMembers = async () => {
return ( return (
<section> <section>
<h2>Styret</h2> <h2>{t("title")}</h2>
<div className={styles.container}> <div className={styles.container}>
{data.map((member: BoardMember) => ( {data.map((member: BoardMember) => (
<article key={member._id} className={styles.card}> <article key={member._id} className={styles.card}>
@@ -73,21 +77,21 @@ const BoardMembers = async () => {
width={256} width={256}
height={256} height={256}
className="circular" className="circular"
alt={`Bilde av styremedlem, ${member.name}`} alt={`${t("imageAlt")}, ${member.name}`}
/> />
</div> </div>
<div className={styles.info}> <div className={styles.info}>
<h3>{member.name}</h3> <h3>{member.name}</h3>
<p>{member.role?.no}</p> <p>{member.role?.[locale]}</p>
</div> </div>
</div> </div>
<p>{member.bio?.no}</p> <p>{member.bio?.[locale]}</p>
<div className={styles.footer}> <div className={styles.footer}>
<p className="text-center"> <p className="text-center">
{member.contactInfo ? ( {member.contactInfo ? (
member.contactInfo member.contactInfo
) : ( ) : (
<i>Ingen kontaktinfo</i> <i>{t("noContact")}</i>
)} )}
</p> </p>
</div> </div>
+6 -2
View File
@@ -1,3 +1,5 @@
import { useTranslations } from "next-intl"
import * as styles from "./contact.module.css" import * as styles from "./contact.module.css"
import { import {
@@ -12,14 +14,16 @@ import {
} from "react-icons/fa" } from "react-icons/fa"
const Contact = () => { const Contact = () => {
const t = useTranslations("contact")
return ( return (
<section> <section>
<h2 className="section-center-header">Finn oss</h2> <h2 className="section-center-header">{t("title")}</h2>
<div className={styles.container}> <div className={styles.container}>
<div className={styles.item}> <div className={styles.item}>
<FaAt /> <FaAt />
<a href="mailto:styret@uiogaming.no" target="_blank" rel="noreferrer"> <a href="mailto:styret@uiogaming.no" target="_blank" rel="noreferrer">
E-post {t("email")}
</a> </a>
</div> </div>
<div className={styles.item}> <div className={styles.item}>
+20 -6
View File
@@ -1,5 +1,6 @@
import { getAllEvents } from "@/lib/sanity" import { getAllEvents } from "@/lib/sanity"
import { getLocale, getTranslations } from "next-intl/server"
import Image from "next/image" import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import { FaCalendarAlt, FaMapMarkerAlt } from "react-icons/fa" import { FaCalendarAlt, FaMapMarkerAlt } from "react-icons/fa"
@@ -18,6 +19,10 @@ interface Event {
} }
const Events = async () => { const Events = async () => {
const t = await getTranslations("events")
const tCommon = await getTranslations("common")
const locale = await getLocale()
const data: Event[] = await getAllEvents() const data: Event[] = await getAllEvents()
const currentDate = new Date() const currentDate = new Date()
@@ -39,7 +44,7 @@ const Events = async () => {
timeZone: "Europe/Oslo", timeZone: "Europe/Oslo",
} }
return date.toLocaleString("no", options) return date.toLocaleString(locale, options)
} }
const mapEvents = (events: Event[]) => { const mapEvents = (events: Event[]) => {
@@ -64,7 +69,7 @@ const Events = async () => {
) : ( ) : (
<> <>
{event.description.substring(0, 70)}...{" "} {event.description.substring(0, 70)}...{" "}
<span className={styles.readMore}>Les mer</span> <span className={styles.readMore}>{t("readMore")}</span>
</> </>
)} )}
</p> </p>
@@ -72,14 +77,21 @@ const Events = async () => {
</Link> </Link>
)) ))
} else { } else {
return <h3 className={styles.noEvents}>Ingen kommende arrangementer</h3> return (
<h3 className={styles.noEvents}>
{t("noEvents")}
<br />
<br />
(っ˘̩╭╮˘̩)っ
</h3>
)
} }
} }
return ( return (
<section> <section>
<div className={styles.container}> <div className={styles.container}>
<h2 className={styles.sectionHeader}>Hva skjer?</h2> <h2 className={styles.sectionHeader}>{t("title")}</h2>
<div className={styles.events}>{mapEvents(filteredEvents)}</div> <div className={styles.events}>{mapEvents(filteredEvents)}</div>
<div className={"no-mobile " + styles.image}> <div className={"no-mobile " + styles.image}>
<Image <Image
@@ -87,9 +99,11 @@ const Events = async () => {
width={850} width={850}
height={480} height={480}
style={{ borderRadius: "1rem", objectFit: "cover", height: "100%" }} style={{ borderRadius: "1rem", objectFit: "cover", height: "100%" }}
alt="Medlemmer spiller brettspill" alt={t("imageAlt")}
/> />
<p className="photo-credit">Foto: 장태민, Studentenes Fotoklubb</p> <p className="photo-credit">
{tCommon("photo")}: 장태민, Studentenes Fotoklubb
</p>
</div> </div>
</div> </div>
</section> </section>
+17 -7
View File
@@ -1,8 +1,18 @@
import { useLocale, useTranslations } from "next-intl"
import Image from "next/image" import Image from "next/image"
import * as styles from "./footer.module.css" import * as styles from "./footer.module.css"
const Footer = () => { const Footer = () => {
const t = useTranslations("footer")
const tCommon = useTranslations("common")
const locale = useLocale()
const statutesUrl =
locale === "en"
? "https://statutes.uiogaming.no"
: "https://vedtekter.uiogaming.no"
return ( return (
<footer className={styles.container}> <footer className={styles.container}>
<div className={styles.footer}> <div className={styles.footer}>
@@ -16,18 +26,18 @@ const Footer = () => {
<div className={styles.links}> <div className={styles.links}>
<ul className="remove-bullets"> <ul className="remove-bullets">
<li> <li>
<a href="https://vedtekter.uiogaming.no" rel="noreferrer"> <a href={statutesUrl} rel="noreferrer">
Vedtekter {tCommon("statutes")}
</a> </a>
</li> </li>
<li> <li>
<a href="https://innmelding.uiogaming.no" rel="noreferrer"> <a href="https://innmelding.uiogaming.no" rel="noreferrer">
Bli medlem {tCommon("join")}
</a> </a>
</li> </li>
<li> <li>
<a href="https://klage.uiogaming.no" rel="noreferrer"> <a href="https://klage.uiogaming.no" rel="noreferrer">
Klage {tCommon("complaint")}
</a> </a>
</li> </li>
<li> <li>
@@ -35,19 +45,19 @@ const Footer = () => {
href="https://github.com/UiO-Gaming/uiogaming.no/" href="https://github.com/UiO-Gaming/uiogaming.no/"
rel="noreferrer" rel="noreferrer"
> >
Kildekode {t("sourceCode")}
</a> </a>
</li> </li>
<li> <li>
<a href="https://admin.uiogaming.no" rel="noreferrer"> <a href="https://admin.uiogaming.no" rel="noreferrer">
Adminområdet {t("admin")}
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
<div className={styles.orgInfo}> <div className={styles.orgInfo}>
<p>Org.Nr.: 925 719 153</p> <p>{t("orgNum")}: 925 719 153</p>
<address>Slemdalsveien 15, 0369 Oslo</address> <address>Slemdalsveien 15, 0369 Oslo</address>
</div> </div>
</div> </div>
+4 -4
View File
@@ -1,8 +1,11 @@
import { useTranslations } from "next-intl"
import Image from "next/image" import Image from "next/image"
import * as styles from "./hero.module.css" import * as styles from "./hero.module.css"
const Hero = () => { const Hero = () => {
const t = useTranslations("hero")
return ( return (
<header id="hjem" className={styles.container}> <header id="hjem" className={styles.container}>
<div className={styles.content}> <div className={styles.content}>
@@ -12,10 +15,7 @@ const Hero = () => {
width={187} width={187}
height={256} height={256}
/> />
<p> <p>{t("text")}</p>
En hobbyforening som ønsker å skape et sosialt miljø rundt gaming og
e-sport på UiO
</p>
</div> </div>
</header> </header>
) )
+6 -10
View File
@@ -1,23 +1,19 @@
"use client" "use client"
import { useTranslations } from "next-intl"
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer" import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer"
import * as styles from "./office.module.css" import * as styles from "./office.module.css"
const Office = () => { const Office = () => {
const t = useTranslations("office")
return ( return (
<section> <section>
<h2>Kontoret</h2> <h2>{t("title")}</h2>
<div className={styles.container}> <div className={styles.container}>
<p> <p>{t("text")}</p>
Vi har et foreningskontor på Chateau Neuf der man kan bruke utstyret <p>{t("tip")}</p>
vårt og henge med andre medlemmer! Vi har blant annet alle de nyeste
konsollene, 6 gaming-PCer samt et bredt utvalg av brettspill.
</p>
<p>
Tips: Du kan kikke rundt i kontoret ved å klikke og dra på bildet
nedenfor 👀
</p>
<div <div
style={{ style={{
width: "100%", width: "100%",
+4 -1
View File
@@ -1,10 +1,13 @@
"use client" "use client"
import { useTranslations } from "next-intl"
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 t = useTranslations("buttons")
const handleBack = () => { const handleBack = () => {
const currentDomain = window.location.hostname const currentDomain = window.location.hostname
const referrer = document.referrer const referrer = document.referrer
@@ -25,7 +28,7 @@ const Back = () => {
onClick={handleBack} onClick={handleBack}
> >
<FaArrowLeft /> <FaArrowLeft />
<p>Tilbake</p> <p>{t("back")}</p>
</button> </button>
) )
} }
+5 -1
View File
@@ -1,6 +1,10 @@
import { useTranslations } from "next-intl"
import * as styles from "./memberButton.module.css" import * as styles from "./memberButton.module.css"
const MemberButton = () => { const MemberButton = () => {
const t = useTranslations("common")
return ( return (
<a <a
href="https://innmelding.uiogaming.no" href="https://innmelding.uiogaming.no"
@@ -8,7 +12,7 @@ const MemberButton = () => {
rel="noreferrer" rel="noreferrer"
className={styles.button} className={styles.button}
> >
Bli medlem {t("join")}
</a> </a>
) )
} }
+17 -4
View File
@@ -2,22 +2,35 @@
import { useState } from "react" import { useState } from "react"
import { useLocale, useTranslations } from "next-intl"
import * as styles from "./otherButton.module.css" import * as styles from "./otherButton.module.css"
const OtherButton = () => { const OtherButton = () => {
const t = useTranslations("about")
const tCommon = useTranslations("common")
const locale = useLocale()
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const toggleDropdown = () => setIsOpen(!isOpen) const toggleDropdown = () => setIsOpen(!isOpen)
const statutesUrl =
locale === "en"
? "https://statutes.uiogaming.no"
: "https://vedtekter.uiogaming.no"
const links = [ const links = [
{ url: "https://vedtekter.uiogaming.no", label: "Vedtekter" }, { url: statutesUrl, label: tCommon("statutes") },
{ url: "https://klage.uiogaming.no", label: "Klage" }, { url: "https://klage.uiogaming.no", label: tCommon("complaint") },
{ url: "https://github.com/UiO-Gaming/dokumenter", label: "Dokumenter" }, {
url: "https://github.com/UiO-Gaming/dokumenter",
label: tCommon("documents"),
},
] ]
return ( return (
<div> <div>
<button onClick={toggleDropdown} className={styles.button}> <button onClick={toggleDropdown} className={styles.button}>
Annet{" "} {t("other")}{" "}
<span className={isOpen ? styles.arrowUp : styles.arrowDown}></span> <span className={isOpen ? styles.arrowUp : styles.arrowDown}></span>
</button> </button>
{isOpen && ( {isOpen && (
+12
View File
@@ -0,0 +1,12 @@
import { getRequestConfig } from "next-intl/server"
import { cookies } from "next/headers"
export default getRequestConfig(async () => {
const cookieStore = await cookies()
const locale = cookieStore.get("locale")?.value || "no"
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
}
})