Compare commits

...

2 Commits

Author SHA1 Message Date
ad515c5be9
Suppress eslint explicit any warnings required for the advanced type trickery to work.
At the call site the types are correct, it is just the type signatures that require any.
2025-04-11 00:53:38 +02:00
c1647ab498
Basic i18n support
Completely TypeScript based translation with support for parameters.
Define the value in messages.ts as a function with arguments, and they will be required at the call site.
Have the full power of TypeScript in the translation so it is possible to do stuff like switching over numbers to write nicer messages for 0/1/n, and so on.
2025-04-11 00:34:55 +02:00
4 changed files with 58 additions and 5 deletions
frontend/src
hooks
lib
studentportalen

@ -0,0 +1,25 @@
import { useProfile } from "./profile.ts";
import { messages } from "../lib/messages.ts";
type MessageType = typeof messages;
type MessageKeys = keyof MessageType;
type MessageParams<T extends MessageKeys> = MessageType[T]["en"] extends (
...args: any // eslint-disable-line @typescript-eslint/no-explicit-any
) => any // eslint-disable-line @typescript-eslint/no-explicit-any
? Parameters<MessageType[T]["en"]>
: [];
export function useI18n() {
const profile = useProfile();
const lang = profile.language;
return function <T extends MessageKeys>(
key: T,
...args: MessageParams<T>
): string {
const message = messages[key][lang] ?? messages[key]["en"];
// @ts-expect-error see https://stackoverflow.com/a/75086839
return typeof message === "function" ? message(...args) : message;
};
}

@ -0,0 +1,23 @@
type AllLanguages<T> = { en: T; sv: T };
export const messages = {
"Home screen": {
en: "Home",
sv: "Aktuellt",
},
"Here you can see the latest and greatest": {
en: "Here you can see the latest and greatest",
sv: "Här ser du allt som är aktuellt",
},
Profile: {
en: "Profile",
sv: "Profil",
},
"Log out": {
en: "Log out",
sv: "Logga ut",
},
} as const satisfies Record<
string,
AllLanguages<string> | AllLanguages<(...args: any) => string> // eslint-disable-line @typescript-eslint/no-explicit-any
>;

@ -1,8 +1,11 @@
import { useI18n } from "../hooks/i18n.ts";
export default function Home() {
const i18n = useI18n();
return (
<>
<h1>Home screen</h1>
<p>Here you can see the latest and greatest</p>
<h1>{i18n("Home screen")}</h1>
<p>{i18n("Here you can see the latest and greatest")}</p>
</>
);
}

@ -1,21 +1,23 @@
import { NavLink } from "react-router";
import "./menu.css";
import { useState } from "react";
import { useI18n } from "../hooks/i18n.ts";
export default function Menu() {
const [subMenuVisible, setSubMenuVisible] = useState(false);
const i18n = useI18n();
return (
<menu className={"main"}>
<li>
<NavLink to={"/"}>Home</NavLink>
<NavLink to={"/"}>{i18n("Home screen")}</NavLink>
</li>
<li>
<button onClick={() => setSubMenuVisible((current) => !current)}>
<span>| | |</span>
</button>
<menu className={"sub " + (subMenuVisible ? "visible" : "")}>
<li>Profile</li>
<li>Log out</li>
<li>{i18n("Profile")}</li>
<li>{i18n("Log out")}</li>
</menu>
</li>
</menu>