feat(i18n): add multi-language UI support with 5 languages
- Add i18n framework with locale detection and cookie persistence - Support English, Traditional Chinese (zh-TW), Simplified Chinese (zh-CN), Japanese (ja), Korean (ko) - Add LanguageSelector component in navigation header - Create locale JSON files with full translations - Update all page components (root, user, results, history) with i18n support - Add client-side translation helpers for dynamic content - Auto-detect user's preferred language from browser settings - Bump version to v0.1.3
This commit is contained in:
parent
e792dfedf1
commit
33301033ae
21 changed files with 1205 additions and 89 deletions
|
|
@ -12,6 +12,7 @@ import {
|
|||
HTTP_ALLOWED,
|
||||
WEBROOT,
|
||||
} from "../helpers/env";
|
||||
import { localeService } from "../i18n/service";
|
||||
|
||||
export let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false;
|
||||
|
||||
|
|
@ -65,51 +66,52 @@ export const userService = new Elysia({ name: "user/service" })
|
|||
|
||||
export const user = new Elysia()
|
||||
.use(userService)
|
||||
.get("/setup", ({ redirect }) => {
|
||||
.use(localeService)
|
||||
.get("/setup", ({ redirect, locale, t }) => {
|
||||
if (!FIRST_RUN) {
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Setup" webroot={WEBROOT}>
|
||||
<BaseHtml title="ConvertX | Setup" webroot={WEBROOT} locale={locale}>
|
||||
<main
|
||||
class={`
|
||||
mx-auto w-full max-w-4xl flex-1 px-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
<h1 class="my-8 text-3xl">Welcome to ConvertX!</h1>
|
||||
<h1 class="my-8 text-3xl">{t("setup", "welcome")}</h1>
|
||||
<article class="article p-0">
|
||||
<header class="w-full bg-neutral-800 p-4">Create your account</header>
|
||||
<header class="w-full bg-neutral-800 p-4">{t("setup", "createYourAccount")}</header>
|
||||
<form method="post" action={`${WEBROOT}/register`} class="p-4">
|
||||
<fieldset class="mb-4 flex flex-col gap-4">
|
||||
<label class="flex flex-col gap-1">
|
||||
Email
|
||||
{t("auth", "email")}
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
class="rounded-sm bg-neutral-800 p-3"
|
||||
placeholder="Email"
|
||||
placeholder={t("auth", "email")}
|
||||
autocomplete="email"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Password
|
||||
{t("auth", "password")}
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
class="rounded-sm bg-neutral-800 p-3"
|
||||
placeholder="Password"
|
||||
placeholder={t("auth", "password")}
|
||||
autocomplete="current-password"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<input type="submit" value="Create account" class="btn-primary" />
|
||||
<input type="submit" value={t("auth", "createAccount")} class="btn-primary" />
|
||||
</form>
|
||||
<footer class="p-4">
|
||||
Report any issues on{" "}
|
||||
{t("setup", "reportIssues")}{" "}
|
||||
<a
|
||||
class={`
|
||||
text-accent-500 underline
|
||||
|
|
@ -117,7 +119,7 @@ export const user = new Elysia()
|
|||
`}
|
||||
href="https://github.com/C4illin/ConvertX"
|
||||
>
|
||||
GitHub
|
||||
{t("setup", "github")}
|
||||
</a>
|
||||
.
|
||||
</footer>
|
||||
|
|
@ -126,19 +128,21 @@ export const user = new Elysia()
|
|||
</BaseHtml>
|
||||
);
|
||||
})
|
||||
.get("/register", ({ redirect }) => {
|
||||
.get("/register", ({ redirect, locale, t }) => {
|
||||
if (!ACCOUNT_REGISTRATION) {
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Register">
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Register" locale={locale}>
|
||||
<>
|
||||
<Header
|
||||
webroot={WEBROOT}
|
||||
accountRegistration={ACCOUNT_REGISTRATION}
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
hideHistory={HIDE_HISTORY}
|
||||
locale={locale}
|
||||
t={t}
|
||||
/>
|
||||
<main
|
||||
class={`
|
||||
|
|
@ -150,29 +154,29 @@ export const user = new Elysia()
|
|||
<form method="post" class="flex flex-col gap-4">
|
||||
<fieldset class="mb-4 flex flex-col gap-4">
|
||||
<label class="flex flex-col gap-1">
|
||||
Email
|
||||
{t("auth", "email")}
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
class="rounded-sm bg-neutral-800 p-3"
|
||||
placeholder="Email"
|
||||
placeholder={t("auth", "email")}
|
||||
autocomplete="email"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Password
|
||||
{t("auth", "password")}
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
class="rounded-sm bg-neutral-800 p-3"
|
||||
placeholder="Password"
|
||||
placeholder={t("auth", "password")}
|
||||
autocomplete="current-password"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<input type="submit" value="Register" class="w-full btn-primary" />
|
||||
<input type="submit" value={t("auth", "registerButton")} class="w-full btn-primary" />
|
||||
</form>
|
||||
</article>
|
||||
</main>
|
||||
|
|
@ -237,7 +241,7 @@ export const user = new Elysia()
|
|||
)
|
||||
.get(
|
||||
"/login",
|
||||
async ({ jwt, redirect, cookie: { auth } }) => {
|
||||
async ({ jwt, redirect, cookie: { auth }, locale, t }) => {
|
||||
if (FIRST_RUN) {
|
||||
return redirect(`${WEBROOT}/setup`, 302);
|
||||
}
|
||||
|
|
@ -254,13 +258,15 @@ export const user = new Elysia()
|
|||
}
|
||||
|
||||
return (
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Login">
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Login" locale={locale}>
|
||||
<>
|
||||
<Header
|
||||
webroot={WEBROOT}
|
||||
accountRegistration={ACCOUNT_REGISTRATION}
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
hideHistory={HIDE_HISTORY}
|
||||
locale={locale}
|
||||
t={t}
|
||||
/>
|
||||
<main
|
||||
class={`
|
||||
|
|
@ -272,23 +278,23 @@ export const user = new Elysia()
|
|||
<form method="post" class="flex flex-col gap-4">
|
||||
<fieldset class="mb-4 flex flex-col gap-4">
|
||||
<label class="flex flex-col gap-1">
|
||||
Email
|
||||
{t("auth", "email")}
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
class="rounded-sm bg-neutral-800 p-3"
|
||||
placeholder="Email"
|
||||
placeholder={t("auth", "email")}
|
||||
autocomplete="email"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Password
|
||||
{t("auth", "password")}
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
class="rounded-sm bg-neutral-800 p-3"
|
||||
placeholder="Password"
|
||||
placeholder={t("auth", "password")}
|
||||
autocomplete="current-password"
|
||||
required
|
||||
/>
|
||||
|
|
@ -301,10 +307,10 @@ export const user = new Elysia()
|
|||
role="button"
|
||||
class="w-full btn-secondary text-center"
|
||||
>
|
||||
Register
|
||||
{t("nav", "register")}
|
||||
</a>
|
||||
) : null}
|
||||
<input type="submit" value="Login" class="w-full btn-primary" />
|
||||
<input type="submit" value={t("auth", "loginButton")} class="w-full btn-primary" />
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
|
|
@ -376,7 +382,7 @@ export const user = new Elysia()
|
|||
})
|
||||
.get(
|
||||
"/account",
|
||||
async ({ user, redirect }) => {
|
||||
async ({ user, redirect, locale, t }) => {
|
||||
if (!user) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
|
@ -388,7 +394,7 @@ export const user = new Elysia()
|
|||
}
|
||||
|
||||
return (
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Account">
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Account" locale={locale}>
|
||||
<>
|
||||
<Header
|
||||
webroot={WEBROOT}
|
||||
|
|
@ -396,6 +402,8 @@ export const user = new Elysia()
|
|||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
hideHistory={HIDE_HISTORY}
|
||||
loggedIn
|
||||
locale={locale}
|
||||
t={t}
|
||||
/>
|
||||
<main
|
||||
class={`
|
||||
|
|
@ -407,41 +415,41 @@ export const user = new Elysia()
|
|||
<form method="post" class="flex flex-col gap-4">
|
||||
<fieldset class="mb-4 flex flex-col gap-4">
|
||||
<label class="flex flex-col gap-1">
|
||||
Email
|
||||
{t("auth", "email")}
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
class="rounded-sm bg-neutral-800 p-3"
|
||||
placeholder="Email"
|
||||
placeholder={t("auth", "email")}
|
||||
autocomplete="email"
|
||||
value={userData.email}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Password (leave blank for unchanged)
|
||||
{t("auth", "newPassword")}
|
||||
<input
|
||||
type="password"
|
||||
name="newPassword"
|
||||
class="rounded-sm bg-neutral-800 p-3"
|
||||
placeholder="Password"
|
||||
placeholder={t("auth", "password")}
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Current Password
|
||||
{t("auth", "currentPassword")}
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
class="rounded-sm bg-neutral-800 p-3"
|
||||
placeholder="Password"
|
||||
placeholder={t("auth", "password")}
|
||||
autocomplete="current-password"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<div role="group">
|
||||
<input type="submit" value="Update" class="w-full btn-primary" />
|
||||
<input type="submit" value={t("auth", "updateButton")} class="w-full btn-primary" />
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue