From 778e8e6b18f679f08d6972a800098247580e787d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veikko=20Lintuj=C3=A4rvi?= Date: Wed, 4 Feb 2026 22:38:23 +0200 Subject: [PATCH] Finnish translation --- ui/src/app.tsx | 12 +++- ui/src/auth/index.ts | 11 ++-- ui/src/auth/server.ts | 27 ++++++--- ui/src/components/Counter.tsx | 3 +- ui/src/components/Error.tsx | 3 +- ui/src/components/Nav.tsx | 69 ++++++++++++++-------- ui/src/i18n.ts | 105 ++++++++++++++++++++++++++++++++++ ui/src/routes/[...404].tsx | 9 +-- ui/src/routes/about.tsx | 8 +-- ui/src/routes/index.tsx | 9 +-- ui/src/routes/login.tsx | 16 +++--- 11 files changed, 211 insertions(+), 61 deletions(-) create mode 100644 ui/src/i18n.ts diff --git a/ui/src/app.tsx b/ui/src/app.tsx index 8a2ec4c..18a00f6 100644 --- a/ui/src/app.tsx +++ b/ui/src/app.tsx @@ -1,11 +1,12 @@ import { type RouteDefinition, Router } from "@solidjs/router"; import { FileRoutes } from "@solidjs/start/router"; -import { MetaProvider } from "@solidjs/meta"; -import { Suspense } from "solid-js"; +import { Meta, MetaProvider } from "@solidjs/meta"; +import { createEffect, Suspense } from "solid-js"; import { querySession } from "./auth"; import Auth from "./components/Context"; import Nav from "./components/Nav"; import ErrorNotification from "./components/Error"; +import { language, t } from "~/i18n"; import "./app.css"; export const route: RouteDefinition = { @@ -13,10 +14,17 @@ export const route: RouteDefinition = { }; export default function App() { + createEffect(() => { + if (typeof document !== "undefined") { + document.documentElement.lang = language(); + } + }); + return ( ( + ); } diff --git a/ui/src/i18n.ts b/ui/src/i18n.ts new file mode 100644 index 0000000..35adf8f --- /dev/null +++ b/ui/src/i18n.ts @@ -0,0 +1,105 @@ +import { createSignal } from "solid-js"; +import { isServer } from "solid-js/web"; + +export type Language = "fi" | "en"; + +const STORAGE_KEY = "ui-language"; + +const translations = { + en: { + "nav.home": "Home", + "nav.about": "About", + "nav.login": "Login", + "nav.signOut": "Sign Out", + "nav.language.fi": "FI", + "nav.language.en": "EN", + "meta.description": "SolidStart with-auth example", + "home.title": "Home", + "home.heading": "Hello World", + "home.signedInAs": "You are signed in as", + "home.logoAlt": "logo", + "about.title": "About", + "about.apiVersion": "API version", + "about.loading": "Loading...", + "login.title": "Sign In", + "login.heading": "Sign in", + "login.orContinueWith": "Or continue with", + "login.signInWithDiscord": "Sign in with Discord", + "login.email": "Email", + "login.password": "Password", + "login.submit": "Submit", + "notFound.title": "Page Not Found", + "notFound.heading": "Not Found", + "notFound.message": "Sorry, the page you’re looking for doesn't exist", + "notFound.goHome": "Go Home", + "error.title": "Error", + "counter.clicks": "Clicks", + "errors.requiredEmailPassword": "Email and password are required", + "errors.invalidStoredPasswordFormat": "Invalid stored password format", + "errors.invalidEmailOrPassword": "Invalid email or password", + "errors.oauthOnly": + "Account exists via OAuth. Sign in with your OAuth provider", + }, + fi: { + "nav.home": "Etusivu", + "nav.about": "Tietoja", + "nav.login": "Kirjaudu", + "nav.signOut": "Kirjaudu ulos", + "nav.language.fi": "FI", + "nav.language.en": "EN", + "meta.description": "SolidStart with-auth -esimerkki", + "home.title": "Etusivu", + "home.heading": "Hei maailma", + "home.signedInAs": "Olet kirjautunut käyttäjänä", + "home.logoAlt": "logo", + "about.title": "Tietoja", + "about.apiVersion": "API-versio", + "about.loading": "Ladataan...", + "login.title": "Kirjaudu", + "login.heading": "Kirjaudu sisään", + "login.orContinueWith": "Tai jatka", + "login.signInWithDiscord": "Kirjaudu Discordilla", + "login.email": "Sähköposti", + "login.password": "Salasana", + "login.submit": "Lähetä", + "notFound.title": "Sivua ei löytynyt", + "notFound.heading": "Ei löytynyt", + "notFound.message": "Valitettavasti etsimääsi sivua ei ole olemassa", + "notFound.goHome": "Takaisin etusivulle", + "error.title": "Virhe", + "counter.clicks": "Klikkauksia", + "errors.requiredEmailPassword": "Sähköposti ja salasana vaaditaan", + "errors.invalidStoredPasswordFormat": + "Tallennetun salasanan muoto on virheellinen", + "errors.invalidEmailOrPassword": "Virheellinen sähköposti tai salasana", + "errors.oauthOnly": "Tili löytyy OAuthin kautta. Kirjaudu OAuth-palvelulla", + }, +} as const; + +export type TranslationKey = keyof typeof translations.en; + +export const normalizeLanguage = (value: unknown): Language => + value === "fi" ? "fi" : "en"; + +const [language, setLanguageSignal] = createSignal("en"); + +if (!isServer) { + const stored = normalizeLanguage(localStorage.getItem(STORAGE_KEY)); + setLanguageSignal(stored); +} + +export const setLanguage = (lang: Language) => { + setLanguageSignal(lang); + if (!isServer) { + localStorage.setItem(STORAGE_KEY, lang); + } +}; + +export { language }; + +export const getTranslations = (lang: Language) => translations[lang]; + +export const t = (key: TranslationKey) => translations[language()][key]; + +export const getLanguageFromFormData = (formData: FormData): Language => + normalizeLanguage(formData.get("lang")); diff --git a/ui/src/routes/[...404].tsx b/ui/src/routes/[...404].tsx index b45332e..e5e8348 100644 --- a/ui/src/routes/[...404].tsx +++ b/ui/src/routes/[...404].tsx @@ -1,16 +1,17 @@ import { Title } from "@solidjs/meta"; +import { t } from "~/i18n"; export default function NotFound() { return (
- Page Not Found -

Not Found

- Sorry, the page you’re looking for doesn't exist + {t("notFound.title")} +

{t("notFound.heading")}

+ {t("notFound.message")} - Go Home + {t("notFound.goHome")}
); diff --git a/ui/src/routes/about.tsx b/ui/src/routes/about.tsx index abbefe5..be22e9f 100644 --- a/ui/src/routes/about.tsx +++ b/ui/src/routes/about.tsx @@ -1,18 +1,18 @@ import { Title } from "@solidjs/meta"; import { createAsync } from "@solidjs/router"; import { Show } from "solid-js"; -import Counter from "~/components/Counter"; import { queryApiVersion } from "~/api"; +import { t } from "~/i18n"; export default function About() { const apiVersion = createAsync(() => queryApiVersion()); return (
- Tietoja + {t("about.title")}

- API version:{" "} - + {t("about.apiVersion")}: + {apiVersion()}

diff --git a/ui/src/routes/index.tsx b/ui/src/routes/index.tsx index 36524df..7036c5a 100644 --- a/ui/src/routes/index.tsx +++ b/ui/src/routes/index.tsx @@ -1,15 +1,16 @@ import { Title } from "@solidjs/meta"; import { useAuth } from "~/components/Context"; +import { t } from "~/i18n"; export default function Home() { const { session } = useAuth(); return (
- Home -

Hello World

- logo - You are signed in as {session()?.email} + {t("home.title")} +

{t("home.heading")}

+ {t("home.logoAlt")} + {t("home.signedInAs")} {session()?.email}
); } diff --git a/ui/src/routes/login.tsx b/ui/src/routes/login.tsx index b5ad86e..d30ee13 100644 --- a/ui/src/routes/login.tsx +++ b/ui/src/routes/login.tsx @@ -4,19 +4,20 @@ import { Show } from "solid-js"; import { useOAuthLogin } from "start-oauth"; import { formLogin } from "~/auth"; import { Discord } from "~/components/Icons"; +import { language, t } from "~/i18n"; export default function Login() { const login = useOAuthLogin(); return (
- Sign In -

Sign in

+ {t("login.title")} +

{t("login.heading")}

- Or continue with + {t("login.orContinueWith")}
- Sign in with Discord + {t("login.signInWithDiscord")}
@@ -37,8 +38,9 @@ function PasswordLogin() { return (
+